maud
maud copied to clipboard
Support returning a Stream
Would be an interesting direction to explore. The main use-case would be streaming HTML to the browsers without blocking on database calls.
let name = futures::future::ok("<p>Maud</p>");
let stream = html_stream! {
h1 { "Greetings, " }
(name)
p { "How are you?" }
};
What's your intended use case for this feature?
My first thought would be to apply pagination or use Ajax calls instead. Streaming raises some difficult design issues that I'd rather not deal with unless there's a strong need for it.
In particular, if the stream raises an error in the middle of rendering a page, then a naive solution would leave the page cut off. That's not a nice user experience IMO. There are ways to handle this problem (e.g. display an error message inline), but the options at that point are more constrained than if all the data was fetched in advance.
(Sorry for the lack of response. I should have asked this question before applying the label.)
What's your intended use case for this feature?
The main use-case is to improve performance - don't want to have a blank page for 2 seconds while the database query is running. Would also want to keep as much of the logic server-side, so no Ajax calls and maintaining state on the client.
I think that if we enforce that all the futures have the type Future<Item = Markup, Error = Markup>
then it is up to the user to decide what to do if there is an error (error message inline, ignore).
I have also encountered this use-case when using home-grown logging interfaces that render large tables of data in pure HTML server-side.
That exact use case might be kind of an anti-pattern, but I'm sympathetic to this change. Mostly because, in theory, it should be feasible to yield valid chunks of HTML tokens as a stream. The question is if it can be done in an elegant way, and if it can work nicely with various frameworks.
On the framework front, async support is uneven. Rocket uses its own interface for streaming responses, for instance. Which frameworks are you targeting?
I was looking to target hyper
, which I kind of got working with #150 and
#[cfg(feature = "hyper")]
mod hyper_support {
use PreEscaped;
use hyper::body::Payload;
use hyper::{Chunk, Error};
use futures::Async;
impl Payload for PreEscaped<String> {
type Data = Chunk;
type Error = Error;
fn poll_data(&mut self) -> Result<Async<Option<Self::Data>>, Self::Error> {
Ok(Async::Ready(Some(Chunk::from(self.0.clone()))))
}
}
}
You can kind of hack this functionality together by using objects that "render" by saving the current length of the buffer, allowing you to split it later:
struct Split<'a> {
inner: Cell<&'a mut usize>
}
impl maud::Render for Split<'_> {
fn render_to(&self, buffer: &mut String) {
// You can avoid using unsafe by using a refcell, but this is an easily understood line
unsafe { **self.inner.as_ptr() = buffer.len(); }
}
}
It's not very convenient, but it works well if you don't have too many yield points.
Why does the buffer argument have the type String
and not Write
?