Suspense boundaries/out of order streaming/anyhow like error handling (#2365)

* create static site generation helpers in the router crate

* work on integrating static site generation into fullstack

* move ssg into a separate crate

* integrate ssg with the launch builder

* simplify ssg example

* fix static_routes for child routes

* move CLI hot reloading websocket code into dioxus-hot-reload

* fix some unused imports

* use the same hot reloading websocket code for fullstack

* fix fullstack hot reloading

* move cli hot reloading logic into the hot reload crate

* ssg example working with dx serve

* add more examples

* fix clippy

* switch to a result for Element

* fix formatting

* fix hot reload doctest imports

* fix axum imports

* add show method to error context

* implement retaining nodes during suspense

* fix unterminated if statements

* communicate between tasks and suspense boundaries

* make suspense placeholders easier to use

* implement IntoDynNode and IntoVNode for more wrappers

* fix clippy examples

* fix rsx tests

* add streaming html utilities to the ssr package

* unify hydration and non-hydration ssr cache

* fix router with Result Element

* don't run server doc tests

* Fix hot reload websocket doc examples

* simple apps working with fullstack streaming

* fix preloading wasm

* Report errors encountered while streaming

* remove async from incremental renderer

* document new VirtualDom suspense methods

* make streaming work with incremental rendering

* fix static site generation

* document suspense structs

* create closure type; allow async event handlers in props; allow shorthand event handlers

* test forwarding event handlers with the shorthand syntax

* fix clippy

* fix imports in spawn async doctest

* fix empty rsx

* fix async result event handlers

* fix mounting router in multiple places

* Fix task dead cancel race condition

* simplify diffing before adding suspense

* fix binary size increase

* fix attribute diffing

* more diffing fixes

* create minimal fullstack feature

* smaller fullstack bundles

* allow mounting nodes that are already created and creating nodes without mounting them

* fix hot reload feature

* fix replacing components

* don't reclaim virtual nodes

* client side suspense working!

* fix CLI

* slightly smaller fullstack builds

* fix multiple suspended scopes

* fix merge errors

* yield back to tokio every few polls to fix suspending on many tasks at once

* remove logs

* document suspense boundary and update suspense example

* fix ssg

* make streaming optional

* fix some router and core tests

* fix suspense example

* fix serialization with out of order server futures

* add incremental streaming hackernews demo

* fix hackernews demo

* fix root hackernews redirect

* fix formatting

* add tests for suspense cases

* slightly smaller binaries

* slightly smaller

* improve error handling docs

* fix errors example link

* fix doc tests

* remove log file

* fix ssr cache type inference

* remove index.html

* fix ssg render template

* fix assigning ids on elements with dynamic attributes

* add desktop feature to the workspace examples

* remove router static generation example; ssg lives in the dioxus-static-generation package

* add a test for effects during suspense

* only run effects on mounted nodes

* fix multiple suspense roots

* fix node iterator

* fix closures without arguments

* fix dioxus-core readme doctest

* remove suspense logs

* fix scope stack

* fix clippy

* remove unused suspense boundary from hackernews

* assert that launch never returns for better compiler errors

* fix static generation launch function

* fix web renderer

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* forward CLI serve settings to fullstack

* re-export serve config at the root of fullstack

* forward env directly instead of using a guard

* just set the port in the CLI for fullstack playwright tests

* fix fullstack dioxus-cli-config feature

* fix launch server merge conflicts

* fix fullstack launch context

* Merge branch 'main' into suspense-2.0

* fix fullstack html data

* remove drop virtual dom feature

* add a comment about only_write_templates binary size workaround

* remove explicit dependencies from use_server_future

* make ErrorContext and SuspenseContext more similar

* Tweak: small tweaks to tomls to make diff smaller

* only rerun components under suspense after the initial placeholders are sent to the client

* add module docs for suspense

* keep track of when suspense boundaries are resolved

* start implementing JS out of order streaming

* fix core tests

* implement the server side of suspense with js

* fix streaming ssr with nested suspense

* move streaming ssr code into fullstack

* revert minification changes

* serialize server future data as the html streams

* start loading scripts wasm immediately instead of defering the script

* very basic nested suspense example working with minimal html updates

* clean up some suspense/error docs

* fix hydrating nested pending server futures

* sort resolved boundaries by height

* Fix disconnecting clients while streaming

* fix static generation crate

* don't insert extra divs when hydrating streamed chunks

* wait to swap in the elements until they are hydrated

* remove inline streaming script

* hackernews partially working

* fix spa mode

* banish the open shadow dom

* fix removing placeholder

* set up streaming playwright test

* run web playwright tests on 9999 to avoid port conflicts with other local servers

* remove suspense nodes if the suspense boundary is replaced before the suspense resolves on the server

* ignore hydration of removed suspense boundaries

* use path based indexing to fix hydrating suspense after parent suspense with child is removed

* re-export dioxus error

* remove resolved suspense divs if the suspense boundary has been removed

* Fix client side initialized server futures

* ignore comment nodes while traversing nodes in core to avoid lists getting swapped out with suspense

* Pass initial hydration data to the client

* hide pre nodes

* don't panic if reclaiming an element fails

* fix scope stack when polling tasks

* improve deserialization out of length message

* Ok(VNode::placeholder()) -> VNode::empty()

* fix typo in rsx usage

* restore testing changes from suspense example

* clean up some logs and comments

* fix playwright tests

* clean up more changes in core

* clean up core tests

* remove anymap dependency

* clean up changes to hooks

* clean up changes in the router, rsx, and web

* revert changes to axum-hello-world

* fix use_server_future

* fix clippy in dioxus-core

* check that the next or previous node exist before checking if we should ignore them

* fix formatting

* fix suspense playwright test

* remove unused suspense code

* add more suspense playwright tests

* add more docs for error boundaries

* fix suspense core tests

* fix ErrorBoundary example

* remove a bunch of debug logging in js

* fix router failure_external_navigation

* use absolute paths in the interpreter build.rs

* strip '\r' while hashing ts files

* add a wrapper with a default error boundary and suspense boundary

* restore hot reloading

* ignore non-ts files when hashing

* sort ts files before hashing them

* fix rsx tests

* fix fullstack doc tests

* fix core tests

* fix axum auth example

* update suspense hydration diagram

* longer playwright build limit

* tiny fixes - spelling, formatting

* update diagram link

* remove comment and template nodes for suspense placeholders

* remove comment nodes as we hydrate text

* simplify hackernews example

* clean up hydrating text nodes

* switch to a separate environment variable for the base path for smaller binaries

* clean up file system html trait

* fix form data

* move streaming code into fullstack

* implement serialize and deserialize for CapturedError

* remove waits in the nested suspense playwright spec

* force sequential fullstack builds for CI

* longer nested suspense delay for CI

* fix --force-sequential flag

* wait to launch server until client build is done

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-07-01 20:50:36 -07:00
1 parent ffa36a67c3
commit 022e4ad203
175 files changed
+7872 -4010

No files matched your search

+213 -126
View File
@@ -2,22 +2,22 @@
//!
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
use crate::Task;
use crate::innerlude::{SuspenseBoundaryProps, Work};
use crate::properties::RootProps;
use crate::root_wrapper::RootScopeWrapper;
use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{
DirtyTasks, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState,
VNodeMount, VProps, WriteMutations,
ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps,
WriteMutations,
},
nodes::RenderReturn,
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::ScopeId,
AttributeValue, ComponentFunction, Element, Event, Mutations, VNode,
AttributeValue, ComponentFunction, Element, Event, Mutations,
};
use crate::{Task, VComponent};
use futures_util::StreamExt;
use rustc_hash::FxHashMap;
use slab::Slab;
use std::collections::BTreeSet;
use std::{any::Any, rc::Rc};
@@ -206,18 +206,25 @@ pub struct VirtualDom {
pub(crate) scopes: Slab<ScopeState>,
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
pub(crate) dirty_tasks: BTreeSet<DirtyTasks>,
// Maps a template path to a map of byte indexes to templates
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
// if hot reload is enabled, we need to keep track of template overrides
#[cfg(debug_assertions)]
pub(crate) templates: rustc_hash::FxHashMap<TemplateId, rustc_hash::FxHashMap<usize, Template>>,
// Otherwise, we just need to keep track of what templates we have registered
#[cfg(not(debug_assertions))]
pub(crate) templates: rustc_hash::FxHashSet<TemplateId>,
// Templates changes that are queued for the next render
pub(crate) queued_templates: Vec<Template>,
// The element ids that are used in the renderer
// These mark a specific place in a whole rsx block
pub(crate) elements: Slab<Option<ElementRef>>,
// Once nodes are mounted, the information about where they are mounted is stored here
// We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events
// Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block
pub(crate) mounts: Slab<VNodeMount>,
pub(crate) runtime: Rc<Runtime>,
@@ -297,7 +304,13 @@ impl VirtualDom {
root: impl ComponentFunction<P, M>,
root_props: P,
) -> Self {
Self::new_with_component(VProps::new(root, |_, _| true, root_props, "root"))
let render_fn = root.id();
let props = VProps::new(root, |_, _| true, root_props, "Root");
Self::new_with_component(VComponent {
name: "root",
render_fn,
props: Box::new(props),
})
}
/// Create a new virtualdom and build it immediately
@@ -309,7 +322,7 @@ impl VirtualDom {
/// Create a new VirtualDom from something that implements [`AnyProps`]
#[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
pub(crate) fn new_with_component(root: VComponent) -> Self {
let (tx, rx) = futures_channel::mpsc::unbounded();
let mut dom = Self {
@@ -317,18 +330,19 @@ impl VirtualDom {
runtime: Runtime::new(tx),
scopes: Default::default(),
dirty_scopes: Default::default(),
dirty_tasks: Default::default(),
templates: Default::default(),
queued_templates: Default::default(),
elements: Default::default(),
mounts: Default::default(),
};
let root = dom.new_scope(Box::new(root), "app");
// Unlike react, we provide a default error boundary that just renders the error as a string
root.state()
.provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
let root = VProps::new(
RootScopeWrapper,
|_, _| true,
RootProps(root),
"RootWrapper",
);
dom.new_scope(Box::new(root), "app");
// the root element is always given element ID 0 since it's the container for the entire tree
dom.elements.insert(None);
@@ -404,7 +418,7 @@ impl VirtualDom {
tracing::Level::TRACE,
"Marking task {:?} (spawned in {:?}) as dirty",
task,
scope.id
scope.id,
);
let order = ScopeOrder::new(scope.height(), scope.id);
@@ -485,7 +499,7 @@ impl VirtualDom {
SchedulerMsg::TaskNotified(id) => {
// Instead of running the task immediately, we insert it into the runtime's task queue.
// The task may be marked dirty at the same time as the scope that owns the task is dropped.
self.mark_task_dirty(id);
self.mark_task_dirty(Task::from_id(id));
}
SchedulerMsg::EffectQueued => {}
};
@@ -497,7 +511,7 @@ impl VirtualDom {
while let Ok(Some(msg)) = self.rx.try_next() {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(task),
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
SchedulerMsg::EffectQueued => {}
}
}
@@ -524,24 +538,18 @@ impl VirtualDom {
// Keep polling tasks until there are no more effects or tasks to run
// Or until we have no more dirty scopes
while !self.dirty_tasks.is_empty() || !self.runtime.pending_effects.borrow().is_empty() {
while !self.runtime.dirty_tasks.borrow().is_empty()
|| !self.runtime.pending_effects.borrow().is_empty()
{
// Next, run any queued tasks
// We choose not to poll the deadline since we complete pretty quickly anyways
while let Some(task) = self.pop_task() {
// Then poll any tasks that might be pending
let mut tasks = task.tasks_queued.into_inner();
while let Some(task) = tasks.pop_front() {
let _ = self.runtime.handle_task_wakeup(task);
let _ = self.runtime.handle_task_wakeup(task);
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
self.queue_events();
if self.has_dirty_scopes() {
// requeue any remaining tasks
for task in tasks {
self.mark_task_dirty(task);
}
return;
}
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
self.queue_events();
if self.has_dirty_scopes() {
return;
}
}
@@ -565,41 +573,45 @@ impl VirtualDom {
/// This will only replace the parent template, not any nested templates.
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
pub fn replace_template(&mut self, template: Template) {
self.register_template_first_byte_index(template);
// we only replace templates if hot reloading is enabled
#[cfg(debug_assertions)]
{
self.register_template_first_byte_index(template);
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() {
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
fn check_node_for_templates(node: &VNode, template: Template) -> bool {
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() {
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
return true;
}
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
return true;
}
for dynamic in node.dynamic_nodes.iter() {
if let crate::DynamicNode::Fragment(nodes) = dynamic {
for node in nodes {
if check_node_for_templates(node, template) {
return true;
for dynamic in node.dynamic_nodes.iter() {
if let crate::DynamicNode::Fragment(nodes) = dynamic {
for node in nodes {
if check_node_for_templates(node, template) {
return true;
}
}
}
}
false
}
false
}
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
if check_node_for_templates(sync, template) {
dirty.push(ScopeId(id));
if let Some(sync) = scope.try_root_node() {
if check_node_for_templates(sync, template) {
dirty.push(ScopeId(id));
}
}
}
}
for dirty in dirty {
self.mark_dirty(dirty);
for dirty in dirty {
self.mark_dirty(dirty);
}
}
}
@@ -647,8 +659,10 @@ impl VirtualDom {
let _runtime = RuntimeGuard::new(self.runtime.clone());
let new_nodes = self.run_scope(ScopeId::ROOT);
self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
// Rebuilding implies we append the created elements to the root
let m = self.create_scope(to, ScopeId::ROOT, new_nodes, None);
let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None);
to.append_children(ElementId(0), m);
}
@@ -666,22 +680,17 @@ impl VirtualDom {
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
let _runtime = RuntimeGuard::new(self.runtime.clone());
while let Some(work) = self.pop_work() {
{
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Then, poll any tasks that might be pending in the scope
for task in work.tasks {
let _ = self.runtime.handle_task_wakeup(task);
match work {
Work::PollTask(task) => {
_ = self.runtime.handle_task_wakeup(task);
// Make sure we process any new events
self.queue_events();
}
self.queue_events();
// If the scope is dirty, run the scope and get the mutations
if work.rerun_scope {
let new_nodes = self.run_scope(work.scope.id);
self.diff_scope(to, work.scope.id, new_nodes);
Work::RerunScope(scope) => {
// If the scope is dirty, run the scope and get the mutations
self.run_and_diff_scope(Some(to), scope.id);
}
}
}
@@ -705,70 +714,127 @@ impl VirtualDom {
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
pub async fn wait_for_suspense(&mut self) {
loop {
if self.runtime.suspended_tasks.get() == 0 {
if !self.suspended_tasks_remaining() {
break;
}
// Wait for a work to be ready (IE new suspense leaves to pop up)
'wait_for_work: loop {
// Process all events - Scopes are marked dirty, etc
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
self.queue_events();
self.wait_for_suspense_work().await;
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
if self.has_dirty_scopes() {
break;
}
self.render_suspense_immediate().await;
}
}
{
// Make sure we set the runtime since we're running user code
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Next, run any queued tasks
// We choose not to poll the deadline since we complete pretty quickly anyways
while let Some(task) = self.pop_task() {
// Then poll any tasks that might be pending
let mut tasks = task.tasks_queued.into_inner();
while let Some(task) = tasks.pop_front() {
if self.runtime.task_runs_during_suspense(task) {
let _ = self.runtime.handle_task_wakeup(task);
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
self.queue_events();
if self.has_dirty_scopes() {
// requeue any remaining tasks
for task in tasks {
self.mark_task_dirty(task);
}
break 'wait_for_work;
}
}
}
}
}
/// Check if there are any suspended tasks remaining
pub fn suspended_tasks_remaining(&self) -> bool {
self.runtime.suspended_tasks.get() > 0
}
self.wait_for_event().await;
/// Wait for the scheduler to have any work that should be run during suspense.
pub async fn wait_for_suspense_work(&mut self) {
// Wait for a work to be ready (IE new suspense leaves to pop up)
loop {
// Process all events - Scopes are marked dirty, etc
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
self.queue_events();
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
if self.has_dirty_scopes() {
break;
}
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
let _runtime = RuntimeGuard::new(self.runtime.clone());
while let Some(work) = self.pop_work() {
// Then, poll any tasks that might be pending in the scope
for task in work.tasks {
{
// Make sure we set the runtime since we're running user code
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Next, run any queued tasks
// We choose not to poll the deadline since we complete pretty quickly anyways
let mut tasks_polled = 0;
while let Some(task) = self.pop_task() {
if self.runtime.task_runs_during_suspense(task) {
let _ = self.runtime.handle_task_wakeup(task);
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
self.queue_events();
if self.has_dirty_scopes() {
return;
}
}
tasks_polled += 1;
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
if tasks_polled > 32 {
yield_now().await;
tasks_polled = 0;
}
}
}
self.wait_for_event().await;
}
}
/// Render any dirty scopes immediately, but don't poll any futures that are client only on that scope
/// Returns a list of suspense boundaries that were resolved
pub async fn render_suspense_immediate(&mut self) -> Vec<ScopeId> {
// Queue any new events before we start working
self.queue_events();
let mut resolved_scopes = Vec::new();
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
let _runtime = RuntimeGuard::new(self.runtime.clone());
let mut work_done = 0;
while let Some(work) = self.pop_work() {
match work {
Work::PollTask(task) => {
// During suspense, we only want to run tasks that are suspended
if self.runtime.task_runs_during_suspense(task) {
let _ = self.runtime.handle_task_wakeup(task);
}
}
Work::RerunScope(scope) => {
if self
.runtime
.get_state(scope.id)
.filter(|scope| scope.should_run_during_suspense())
.is_some()
{
let scope_state = self.get_scope(scope.id).unwrap();
let was_suspended =
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
.filter(|props| props.suspended())
.is_some();
// If the scope is dirty, run the scope and get the mutations
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope.id);
let scope_state = self.get_scope(scope.id).unwrap();
let is_now_suspended =
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
.filter(|props| props.suspended())
.is_some();
self.queue_events();
// If the scope is dirty, run the scope and get the mutations
if work.rerun_scope {
let new_nodes = self.run_scope(work.scope.id);
self.diff_scope(&mut NoOpMutations, work.scope.id, new_nodes);
if is_now_suspended {
resolved_scopes.retain(|&id| id != scope.id);
} else if was_suspended {
resolved_scopes.push(scope.id);
}
} else {
tracing::warn!(
"Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
scope.id
);
}
}
}
// Queue any new events
self.queue_events();
work_done += 1;
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
if work_done > 32 {
yield_now().await;
work_done = 0;
}
}
resolved_scopes.sort_by_key(|&id| self.runtime.get_state(id).unwrap().height);
resolved_scopes
}
/// Get the current runtime
@@ -817,7 +883,11 @@ impl VirtualDom {
while let Some(path) = parent {
let mut listeners = vec![];
let el_ref = &self.mounts[path.mount.0].node;
let Some(mount) = self.mounts.get(path.mount.0) else {
// If the node is suspended and not mounted, we can just ignore the event
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let target_path = path.path;
@@ -827,9 +897,7 @@ impl VirtualDom {
for attr in attrs.iter() {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(this_path)
{
if attr.name.get(2..) == Some(name) && target_path.is_decendant(this_path) {
listeners.push(&attr.value);
// Break if this is the exact target element.
@@ -874,7 +942,11 @@ impl VirtualDom {
name = "VirtualDom::handle_non_bubbling_event"
)]
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
let el_ref = &self.mounts[node.mount.0].node;
let Some(mount) = self.mounts.get(node.mount.0) else {
// If the node is suspended and not mounted, we can just ignore the event
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let target_path = node.path;
@@ -884,7 +956,7 @@ impl VirtualDom {
for attr in attrs.iter() {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if attr.name.get(2..) == Some(name) && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
self.runtime.rendering.set(false);
listener.call(uievent.clone());
@@ -907,3 +979,18 @@ impl Drop for VirtualDom {
}
}
}
/// Yield control back to the async scheduler. This is used to give the scheduler a chance to run other pending work. Or cancel the task if the client has disconnected.
async fn yield_now() {
let mut yielded = false;
std::future::poll_fn::<(), _>(move |cx| {
if !yielded {
cx.waker().wake_by_ref();
yielded = true;
std::task::Poll::Pending
} else {
std::task::Poll::Ready(())
}
})
.await;
}