Polled SSR Stream (#2824)

* Switch to pinned channels.

* Fix ServerRenderer so it's not blocked until the result is resolved.

* Fix tests.

* Remove unused SendError.

* Implement a stream to be polled alongside rendering.

* Update Buffer Size.

* Make Send renderer work.

* Remove pinned channels.

* Unified Naming.

* Optimise code.

* Restore capacity.

* Remove unused profile.

* Default to separate resolver.

* Reduce allocations on string.

* Adjust API.

* Remove duplicate trait bound.

* Update docs.

* Remove capacity setting.

* Unsafe?

* Separate files.

* Adjust inlining.

* Fix test.

* Update notice.

* Update documentation.

* Fix tests.
This commit is contained in:
Kaede Hoshikawa 2022-09-11 05:25:00 +09:00 committed by GitHub
parent 9484249948
commit 278d2ce08e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 509 additions and 282 deletions

View File

@ -31,9 +31,8 @@ implicit-clone = { version = "0.3", features = ["map"] }
base64ct = { version = "1.5.0", features = ["std"], optional = true }
bincode = { version = "1.3.3", optional = true }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1.19", features = ["sync"] }
tokio-stream = { version = "0.1.9", features = ["sync"] }
tracing = "0.1.36"
pin-project = "1.0.11"
[dependencies.web-sys]
version = "^0.3.59"
@ -77,8 +76,10 @@ wasm-bindgen-futures = "0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
num_cpus = { version = "1.13", optional = true }
tokio-util = { version = "0.7", features = ["rt"], optional = true }
once_cell = "1"
tokio = { version = "1.19", features = ["rt", "time"], optional = true }
tokio-stream = { version = "0.1", features = ["time"], optional = true }
tokio-util = { version = "0.7", features = ["rt"], optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3"
@ -95,7 +96,7 @@ features = [
]
[features]
tokio = ["tokio/rt", "tokio/time", "dep:num_cpus", "dep:tokio-util"]
tokio = ["dep:tokio", "dep:num_cpus", "dep:tokio-util", "dep:tokio-stream"]
ssr = ["dep:html-escape", "dep:base64ct", "dep:bincode"]
csr = []
hydration = ["csr", "dep:bincode"]

View File

@ -291,11 +291,13 @@ impl<COMP: BaseComponent> Scope<COMP> {
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::html::component::lifecycle::{
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
};
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
use crate::platform::pinned::oneshot;
use crate::scheduler;
use crate::virtual_dom::Collectable;
@ -342,9 +344,9 @@ mod feat_ssr {
.await;
if let Some(prepared_state) = self.get_component().unwrap().prepare_state() {
w.write(r#"<script type="application/x-yew-comp-state">"#.into());
w.write(prepared_state.into());
w.write(r#"</script>"#.into());
let _ = w.write_str(r#"<script type="application/x-yew-comp-state">"#);
let _ = w.write_str(&prepared_state);
let _ = w.write_str(r#"</script>"#);
}
if hydratable {

View File

@ -0,0 +1,212 @@
use std::cell::UnsafeCell;
use std::fmt::{self, Write};
use std::marker::PhantomData;
use std::rc::Rc;
use std::task::{Poll, Waker};
use futures::stream::{FusedStream, Stream};
static BUF_SIZE: usize = 1024;
enum BufStreamState {
Ready,
Pending(Waker),
Done,
}
struct Inner {
buf: String,
state: BufStreamState,
// This type is not send or sync.
_marker: PhantomData<Rc<()>>,
}
impl Inner {
#[inline]
const fn new() -> Self {
Self {
buf: String::new(),
state: BufStreamState::Ready,
_marker: PhantomData,
}
}
#[inline]
fn wake(&mut self) {
if let BufStreamState::Pending(ref waker) = self.state {
waker.wake_by_ref();
self.state = BufStreamState::Ready;
}
}
#[inline]
fn buf_reserve(&mut self) {
if self.buf.is_empty() {
self.buf.reserve(BUF_SIZE);
}
}
}
impl Write for Inner {
fn write_str(&mut self, s: &str) -> fmt::Result {
if s.is_empty() {
return Ok(());
}
self.wake();
if s.len() < BUF_SIZE {
self.buf_reserve();
}
self.buf.write_str(s)
}
fn write_char(&mut self, c: char) -> fmt::Result {
self.wake();
self.buf_reserve();
self.buf.write_char(c)
}
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
self.wake();
self.buf_reserve();
self.buf.write_fmt(args)
}
}
/// An asynchronous [`String`] writer.
///
/// This type implements [`fmt::Write`] and can be used with [`write!`] and [`writeln!`].
pub(crate) struct BufWriter {
inner: Rc<UnsafeCell<Inner>>,
}
impl Write for BufWriter {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &mut *self.inner.get() };
inner.write_str(s)
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &mut *self.inner.get() };
inner.write_char(c)
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &mut *self.inner.get() };
inner.write_fmt(args)
}
}
impl Drop for BufWriter {
fn drop(&mut self) {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &mut *self.inner.get() };
inner.wake();
inner.state = BufStreamState::Done;
}
}
/// An asynchronous [`String`] reader.
pub(crate) struct BufReader {
inner: Rc<UnsafeCell<Inner>>,
}
impl Stream for BufReader {
type Item = String;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &mut *self.inner.get() };
if !inner.buf.is_empty() {
let buf = std::mem::take(&mut inner.buf);
return Poll::Ready(Some(buf));
}
if let BufStreamState::Done = inner.state {
return Poll::Ready(None);
}
inner.state = BufStreamState::Pending(cx.waker().clone());
Poll::Pending
}
}
impl FusedStream for BufReader {
fn is_terminated(&self) -> bool {
// SAFETY:
//
// We can acquire a mutable reference without checking as:
//
// - This type is !Sync and !Send.
// - This function is not used by any other functions that has access to the inner type.
// - The mutable reference is dropped at the end of this function.
let inner = unsafe { &*self.inner.get() };
matches!(
(&inner.state, inner.buf.is_empty()),
(BufStreamState::Done, true)
)
}
}
/// Creates an asynchronous buffer that operates over String.
pub(crate) fn buffer() -> (BufWriter, BufReader) {
let inner = Rc::new(UnsafeCell::new(Inner::new()));
let w = {
let inner = inner.clone();
BufWriter { inner }
};
let r = BufReader { inner };
(w, r)
}

View File

@ -0,0 +1,70 @@
//! Asynchronous utilities to work with `String`s.
use std::future::Future;
use futures::future::{self, MaybeDone};
use futures::stream::{FusedStream, Stream};
use futures::StreamExt;
use pin_project::pin_project;
mod buffer;
pub(crate) use buffer::{buffer, BufReader, BufWriter};
/// A buffered asynchronous [`String`] [`Stream`].
///
/// A BufStream combines a BufWriter - BufReader pair and a resolving future that writes to the
/// buffer and polls the future alongside the buffer.
#[pin_project]
pub(crate) struct BufStream<F>
where
F: Future<Output = ()>,
{
#[pin]
resolver: MaybeDone<F>,
inner: BufReader,
}
impl<F> BufStream<F>
where
F: Future<Output = ()>,
{
/// Creates a `BufStream`.
pub fn new<C>(f: C) -> Self
where
C: FnOnce(BufWriter) -> F,
{
let (w, r) = buffer();
let resolver = future::maybe_done(f(w));
BufStream { inner: r, resolver }
}
}
impl<F> Stream for BufStream<F>
where
F: Future<Output = ()>,
{
type Item = String;
#[inline]
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.project();
let _ = this.resolver.poll(cx);
this.inner.poll_next_unpin(cx)
}
}
impl<F> FusedStream for BufStream<F>
where
F: Future<Output = ()>,
{
#[inline]
fn is_terminated(&self) -> bool {
self.inner.is_terminated()
}
}

View File

@ -1,103 +0,0 @@
//! This module contains types for I/O functionality.
// This module should remain private until impl trait type alias becomes available so
// `BufReader` can be produced with an existential type.
use std::borrow::Cow;
use futures::stream::Stream;
use crate::platform::sync::mpsc::{self, UnboundedReceiverStream, UnboundedSender};
// Same as std::io::BufWriter and futures::io::BufWriter.
pub(crate) const DEFAULT_BUF_SIZE: usize = 8 * 1024;
/// A [`futures::io::BufWriter`], but operates over string and yields into a Stream.
pub(crate) struct BufWriter {
buf: String,
tx: UnboundedSender<String>,
capacity: usize,
}
/// Creates a Buffer pair.
pub(crate) fn buffer(capacity: usize) -> (BufWriter, impl Stream<Item = String>) {
let (tx, rx) = mpsc::unbounded_channel::<String>();
let tx = BufWriter {
buf: String::with_capacity(capacity),
tx,
capacity,
};
(tx, UnboundedReceiverStream::new(rx))
}
// Implementation Notes:
//
// When jemalloc is used and a reasonable buffer length is chosen,
// performance of this buffer is related to the number of allocations
// instead of the amount of memory that is allocated.
//
// A Bytes-based implementation is also tested, and yielded a similar performance to String-based
// buffer.
//
// Having a String-based buffer avoids unsafe / cost of conversion between String and Bytes
// when text based content is needed (e.g.: post-processing).
//
// `Bytes::from` can be used to convert a `String` to `Bytes` if web server asks for an
// `impl Stream<Item = Bytes>`. This conversion incurs no memory allocation.
//
// Yielding the output with a Stream provides a couple advantages:
//
// 1. All child components of a VList can have their own buffer and be rendered concurrently.
// 2. If a fixed buffer is used, the rendering process can become blocked if the buffer is filled.
// Using a stream avoids this side effect and allows the renderer to finish rendering
// without being actively polled.
impl BufWriter {
#[inline]
pub fn capacity(&self) -> usize {
self.capacity
}
fn drain(&mut self) {
let _ = self.tx.send(self.buf.drain(..).collect());
self.buf.reserve(self.capacity);
}
/// Returns `True` if the internal buffer has capacity to fit a string of certain length.
#[inline]
fn has_capacity_of(&self, next_part_len: usize) -> bool {
self.buf.capacity() >= self.buf.len() + next_part_len
}
/// Writes a string into the buffer, optionally drains the buffer.
pub fn write(&mut self, s: Cow<'_, str>) {
if !self.has_capacity_of(s.len()) {
// There isn't enough capacity, we drain the buffer.
self.drain();
}
if self.has_capacity_of(s.len()) {
// The next part is going to fit into the buffer, we push it onto the buffer.
self.buf.push_str(&s);
} else {
// if the next part is more than buffer size, we send the next part.
// We don't need to drain the buffer here as the result of self.has_capacity_of() only
// changes if the buffer was drained. If the buffer capacity didn't change,
// then it means self.has_capacity_of() has returned true the first time which will be
// guaranteed to be matched by the left hand side of this implementation.
let _ = self.tx.send(s.into_owned());
}
}
}
impl Drop for BufWriter {
fn drop(&mut self) {
if !self.buf.is_empty() {
let mut buf = String::new();
std::mem::swap(&mut buf, &mut self.buf);
let _ = self.tx.send(buf);
}
}
}

View File

@ -45,10 +45,9 @@ use std::io::Result;
use std::marker::PhantomData;
#[cfg(feature = "ssr")]
pub(crate) mod io;
pub(crate) mod fmt;
pub mod pinned;
pub mod sync;
pub mod time;
#[cfg(target_arch = "wasm32")]

View File

@ -38,6 +38,9 @@ struct Inner<T> {
closed: bool,
sender_ctr: usize,
items: VecDeque<T>,
// This type is not send or sync.
_marker: PhantomData<Rc<()>>,
}
impl<T> Inner<T> {
@ -287,6 +290,7 @@ pub fn unbounded<T>() -> (UnboundedSender<T>, UnboundedReceiver<T>) {
sender_ctr: 1,
items: VecDeque::new(),
_marker: PhantomData,
}));
(

View File

@ -20,6 +20,9 @@ struct Inner<T> {
rx_waker: Option<Waker>,
closed: bool,
item: Option<T>,
// This type is not send or sync.
_marker: PhantomData<Rc<()>>,
}
/// The receiver of a oneshot channel.
@ -138,6 +141,8 @@ pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
rx_waker: None,
closed: false,
item: None,
_marker: PhantomData,
}));
(

View File

@ -108,27 +108,38 @@ impl Runtime {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use futures::channel::oneshot;
use once_cell::sync::Lazy;
use tokio::sync::Barrier;
use tokio::test;
use tokio::time::timeout;
use super::*;
static RUNTIME_2: Lazy<Runtime> =
Lazy::new(|| Runtime::new(2).expect("failed to create runtime."));
#[test]
async fn test_spawn_pinned_least_busy() {
let runtime = Runtime::new(2).expect("failed to create runtime.");
let (tx1, rx1) = oneshot::channel();
let (tx2, rx2) = oneshot::channel();
runtime.spawn_pinned(move || async move {
tx1.send(std::thread::current().id())
.expect("failed to send!");
});
let bar = Arc::new(Barrier::new(2));
runtime.spawn_pinned(move || async move {
{
let bar = bar.clone();
RUNTIME_2.spawn_pinned(move || async move {
bar.wait().await;
tx1.send(std::thread::current().id())
.expect("failed to send!");
});
}
RUNTIME_2.spawn_pinned(move || async move {
bar.wait().await;
tx2.send(std::thread::current().id())
.expect("failed to send!");
});
@ -148,7 +159,7 @@ mod tests {
#[test]
async fn test_spawn_local_within_send() {
let runtime = Runtime::new(1).expect("failed to create runtime.");
let runtime = Runtime::default();
let (tx, rx) = oneshot::channel();

View File

@ -1,5 +0,0 @@
//! A module that provides task synchronisation primitives.
#[doc(inline)]
pub use tokio::sync::oneshot;
pub mod mpsc;

View File

@ -1,6 +0,0 @@
//! A multi-producer, single-receiver channel.
#[doc(inline)]
pub use tokio::sync::mpsc::*;
#[doc(inline)]
pub use tokio_stream::wrappers::{ReceiverStream, UnboundedReceiverStream};

View File

@ -1,11 +1,13 @@
use std::fmt;
use std::future::Future;
use futures::pin_mut;
use futures::stream::{Stream, StreamExt};
use tracing::Instrument;
use crate::html::{BaseComponent, Scope};
use crate::platform::io::{self, DEFAULT_BUF_SIZE};
use crate::platform::{spawn_local, LocalHandle, Runtime};
use crate::platform::fmt::BufStream;
use crate::platform::{LocalHandle, Runtime};
/// A Yew Server-side Renderer that renders on the current thread.
///
@ -13,9 +15,9 @@ use crate::platform::{spawn_local, LocalHandle, Runtime};
///
/// This renderer does not spawn its own runtime and can only be used when:
///
/// - `wasm-bindgen` is selected as the backend of Yew runtime.
/// - `wasm-bindgen-futures` is selected as the backend of Yew runtime.
/// - running within a [`Runtime`](crate::platform::Runtime).
/// - running within a tokio [`LocalSet`](tokio::task::LocalSet).
/// - running within a tokio [`LocalSet`](struct@tokio::task::LocalSet).
#[cfg(feature = "ssr")]
#[derive(Debug)]
pub struct LocalServerRenderer<COMP>
@ -24,7 +26,6 @@ where
{
props: COMP::Properties,
hydratable: bool,
capacity: usize,
}
impl<COMP> Default for LocalServerRenderer<COMP>
@ -57,19 +58,9 @@ where
Self {
props,
hydratable: true,
capacity: DEFAULT_BUF_SIZE,
}
}
/// Sets the capacity of renderer buffer.
///
/// Default: `8192`
pub fn capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
/// Sets whether an the rendered result is hydratable.
///
/// Defaults to `true`.
@ -84,16 +75,16 @@ where
/// Renders Yew Application.
pub async fn render(self) -> String {
let mut s = String::new();
let s = self.render_stream();
futures::pin_mut!(s);
self.render_to_string(&mut s).await;
s
s.collect().await
}
/// Renders Yew Application to a String.
pub async fn render_to_string(self, w: &mut String) {
let mut s = self.render_stream();
let s = self.render_stream();
futures::pin_mut!(s);
while let Some(m) = s.next().await {
w.push_str(&m);
@ -105,29 +96,26 @@ where
level = tracing::Level::DEBUG,
name = "render",
skip(self),
fields(hydratable = self.hydratable, capacity = self.capacity),
fields(hydratable = self.hydratable),
)]
pub fn render_stream(self) -> impl Stream<Item = String> {
let (mut w, r) = io::buffer(self.capacity);
let scope = Scope::<COMP>::new(None);
let outer_span = tracing::Span::current();
spawn_local(async move {
BufStream::new(move |mut w| async move {
let render_span = tracing::debug_span!("render_stream_item");
render_span.follows_from(outer_span);
scope
.render_into_stream(&mut w, self.props.into(), self.hydratable)
.instrument(render_span)
.await;
});
r
})
}
}
/// A Yew Server-side Renderer.
///
/// This renderer spawns the rendering task to an internal worker pool and receives result when
/// This renderer spawns the rendering task to a Yew [`Runtime`]. and receives result when
/// the rendering process has finished.
///
/// See [`yew::platform`] for more information.
@ -138,7 +126,6 @@ where
{
create_props: Box<dyn Send + FnOnce() -> COMP::Properties>,
hydratable: bool,
capacity: usize,
rt: Option<Runtime>,
}
@ -189,7 +176,6 @@ where
Self {
create_props: Box::new(create_props),
hydratable: true,
capacity: DEFAULT_BUF_SIZE,
rt: None,
}
}
@ -201,15 +187,6 @@ where
self
}
/// Sets the capacity of renderer buffer.
///
/// Default: `8192`
pub fn capacity(mut self, capacity: usize) -> Self {
self.capacity = capacity;
self
}
/// Sets whether an the rendered result is hydratable.
///
/// Defaults to `true`.
@ -224,11 +201,26 @@ where
/// Renders Yew Application.
pub async fn render(self) -> String {
let mut s = String::new();
let Self {
create_props,
hydratable,
rt,
} = self;
self.render_to_string(&mut s).await;
let (tx, rx) = futures::channel::oneshot::channel();
let create_task = move || async move {
let props = create_props();
let s = LocalServerRenderer::<COMP>::with_props(props)
.hydratable(hydratable)
.render()
.await;
s
let _ = tx.send(s);
};
Self::spawn_rendering_task(rt, create_task);
rx.await.expect("failed to render application")
}
/// Renders Yew Application to a String.
@ -240,25 +232,12 @@ where
}
}
/// Renders Yew Application into a string Stream.
pub fn render_stream(self) -> impl Send + Stream<Item = String> {
let Self {
create_props,
hydratable,
capacity,
rt,
} = self;
let (mut w, r) = io::buffer(capacity);
let create_task = move || async move {
let props = create_props();
let scope = Scope::<COMP>::new(None);
scope
.render_into_stream(&mut w, props.into(), hydratable)
.await;
};
#[inline]
fn spawn_rendering_task<F, Fut>(rt: Option<Runtime>, create_task: F)
where
F: 'static + Send + FnOnce() -> Fut,
Fut: Future<Output = ()> + 'static,
{
match rt {
// If a runtime is specified, spawn to the specified runtime.
Some(m) => m.spawn_pinned(create_task),
@ -269,7 +248,31 @@ where
None => Runtime::default().spawn_pinned(create_task),
},
}
}
r
/// Renders Yew Application into a string Stream.
pub fn render_stream(self) -> impl Send + Stream<Item = String> {
let Self {
create_props,
hydratable,
rt,
} = self;
let (tx, rx) = futures::channel::mpsc::unbounded();
let create_task = move || async move {
let props = create_props();
let s = LocalServerRenderer::<COMP>::with_props(props)
.hydratable(hydratable)
.render_stream();
pin_mut!(s);
while let Some(m) = s.next().await {
let _ = tx.unbounded_send(m);
}
};
Self::spawn_rendering_task(rt, create_task);
rx
}
}

View File

@ -111,36 +111,42 @@ pub(crate) use feat_ssr_hydration::*;
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
impl Collectable {
pub(crate) fn write_open_tag(&self, w: &mut BufWriter) {
w.write("<!--".into());
w.write(self.open_start_mark().into());
let _ = w.write_str("<!--");
let _ = w.write_str(self.open_start_mark());
#[cfg(debug_assertions)]
match self {
Self::Component(type_name) => w.write((*type_name).into()),
Self::Component(type_name) => {
let _ = w.write_str(*type_name);
}
Self::Suspense => {}
}
w.write(self.end_mark().into());
w.write("-->".into());
let _ = w.write_str(self.end_mark());
let _ = w.write_str("-->");
}
pub(crate) fn write_close_tag(&self, w: &mut BufWriter) {
w.write("<!--".into());
w.write(self.close_start_mark().into());
let _ = w.write_str("<!--");
let _ = w.write_str(self.close_start_mark());
#[cfg(debug_assertions)]
match self {
Self::Component(type_name) => w.write((*type_name).into()),
Self::Component(type_name) => {
let _ = w.write_str(*type_name);
}
Self::Suspense => {}
}
w.write(self.end_mark().into());
w.write("-->".into());
let _ = w.write_str(self.end_mark());
let _ = w.write_str("-->");
}
}
}

View File

@ -20,7 +20,7 @@ use crate::html::{AnyScope, Scope};
#[cfg(feature = "csr")]
use crate::html::{NodeRef, Scoped};
#[cfg(feature = "ssr")]
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
/// A virtual component.
pub struct VComp {

View File

@ -156,6 +156,7 @@ mod test {
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use std::task::Poll;
use futures::stream::StreamExt;
@ -163,7 +164,7 @@ mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::platform::io::{self, BufWriter};
use crate::platform::fmt::{self, BufWriter};
impl VList {
pub(crate) async fn render_into_stream(
@ -186,7 +187,6 @@ mod feat_ssr {
) where
I: Iterator<Item = &'a VNode>,
{
let buf_capacity = w.capacity();
let mut w = w;
while let Some(m) = children.next() {
let child_fur = async move {
@ -203,7 +203,7 @@ mod feat_ssr {
match poll!(child_fur.as_mut()) {
Poll::Pending => {
let (mut next_w, next_r) = io::buffer(buf_capacity);
let (mut next_w, next_r) = fmt::buffer();
// Move buf writer into an async block for it to be dropped at
// the end of the future.
let rest_render_fur = async move {
@ -223,7 +223,7 @@ mod feat_ssr {
pin_mut!(next_r);
while let Some(m) = next_r.next().await {
w.write(m.into());
let _ = w.write_str(m.as_str());
}
};

View File

@ -153,7 +153,7 @@ mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
impl VNode {
pub(crate) fn render_into_stream<'a>(

View File

@ -28,7 +28,7 @@ impl VSuspense {
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::Collectable;
impl VSuspense {

View File

@ -440,9 +440,11 @@ impl PartialEq for VTag {
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::html::AnyScope;
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
use crate::virtual_dom::VText;
// Elements that cannot have any child elements.
@ -458,17 +460,17 @@ mod feat_ssr {
parent_scope: &AnyScope,
hydratable: bool,
) {
w.write("<".into());
w.write(self.tag().into());
let _ = w.write_str("<");
let _ = w.write_str(self.tag());
let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| {
w.write(" ".into());
w.write(name.into());
let _ = w.write_str(" ");
let _ = w.write_str(name);
if let Some(m) = val {
w.write("=\"".into());
w.write(html_escape::encode_double_quoted_attribute(m));
w.write("\"".into());
let _ = w.write_str("=\"");
let _ = w.write_str(&*html_escape::encode_double_quoted_attribute(m));
let _ = w.write_str("\"");
}
};
@ -486,7 +488,7 @@ mod feat_ssr {
write_attr(w, k, Some(v));
}
w.write(">".into());
let _ = w.write_str(">");
match self.inner {
VTagInner::Input(_) => {}
@ -497,7 +499,7 @@ mod feat_ssr {
.await;
}
w.write("</textarea>".into());
let _ = w.write_str("</textarea>");
}
VTagInner::Other {
ref tag,
@ -509,9 +511,9 @@ mod feat_ssr {
.render_into_stream(w, parent_scope, hydratable)
.await;
w.write(Cow::Borrowed("</"));
w.write(Cow::Borrowed(tag));
w.write(Cow::Borrowed(">"));
let _ = w.write_str("</");
let _ = w.write_str(tag);
let _ = w.write_str(">");
} else {
// We don't write children of void elements nor closing tags.
debug_assert!(children.is_empty(), "{} cannot have any children!", tag);

View File

@ -35,9 +35,11 @@ impl PartialEq for VText {
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::html::AnyScope;
use crate::platform::io::BufWriter;
use crate::platform::fmt::BufWriter;
impl VText {
pub(crate) async fn render_into_stream(
@ -47,7 +49,7 @@ mod feat_ssr {
_hydratable: bool,
) {
let s = html_escape::encode_text(&self.text);
w.write(s);
let _ = w.write_str(&*s);
}
}
}

134
tools/Cargo.lock generated
View File

@ -23,10 +23,19 @@ dependencies = [
]
[[package]]
name = "anyhow"
version = "1.0.58"
name = "android_system_properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "508b352bb5c066aac251f6daf6b36eccd03e8a88e8081cd44959ea277a3af9a8"
[[package]]
name = "anymap2"
@ -119,9 +128,9 @@ checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
[[package]]
name = "bytes"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cc"
@ -156,22 +165,24 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.19"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
checksum = "3f725f340c3854e3cb3ab736dc21f0cca183303acea3b3ffec30f141503ac8eb"
dependencies = [
"libc",
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "clap"
version = "3.2.13"
version = "3.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac2bd7a1eb07da9ac757c923f69373deb7bc2ba5efc951b873bcb5e693992dca"
checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9"
dependencies = [
"atty",
"bitflags",
@ -186,9 +197,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.2.7"
version = "3.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
dependencies = [
"heck",
"proc-macro-error",
@ -290,9 +301,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
@ -789,6 +800,19 @@ dependencies = [
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf7d67cf4a22adc5be66e75ebdf769b3f2ea032041437a7061f97a63dad4b"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -851,9 +875,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
[[package]]
name = "itoa"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "jemalloc-sys"
@ -924,9 +948,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.126"
version = "0.2.131"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40"
[[package]]
name = "libgit2-sys"
@ -944,9 +968,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.3"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da83a57f3f5ba3680950aa3cbc806fc297bc0b289d42e8942ed528ace71b8145"
checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565"
[[package]]
name = "libssh2-sys"
@ -1228,9 +1252,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "prettyplease"
version = "0.1.16"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da6ffbe862780245013cb1c0a48c4e44b7d665548088f91f6b90876d0625e4c2"
checksum = "697ae720ee02011f439e0701db107ffe2916d83f718342d65d7f8bf7b8a5fee9"
dependencies = [
"proc-macro2",
"syn",
@ -1262,9 +1286,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.40"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
"unicode-ident",
]
@ -1280,9 +1304,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.20"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
@ -1319,9 +1343,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.2.13"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
]
@ -1406,15 +1430,15 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.8"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8"
checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
[[package]]
name = "ryu"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "schannel"
@ -1463,15 +1487,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.12"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711"
[[package]]
name = "serde"
version = "1.0.139"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553"
dependencies = [
"serde_derive",
]
@ -1490,9 +1514,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.139"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391"
dependencies = [
"proc-macro2",
"quote",
@ -1501,9 +1525,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.82"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7"
dependencies = [
"itoa",
"ryu",
@ -1573,9 +1597,9 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.24.2"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
@ -1586,9 +1610,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
dependencies = [
"proc-macro2",
"quote",
@ -1657,18 +1681,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
dependencies = [
"proc-macro2",
"quote",
@ -1703,9 +1727,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.20.0"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e"
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
dependencies = [
"autocfg",
"bytes",
@ -1752,7 +1776,6 @@ dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
@ -1823,9 +1846,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
[[package]]
name = "unicode-normalization"
@ -1923,9 +1946,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.31"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f"
checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad"
dependencies = [
"cfg-if",
"js-sys",
@ -2122,6 +2145,7 @@ dependencies = [
"js-sys",
"num_cpus",
"once_cell",
"pin-project",
"serde",
"slab",
"thiserror",