From b6920fad4b0e87b67671d682700693052be71b12 Mon Sep 17 00:00:00 2001 From: Claas Date: Sat, 27 Apr 2024 20:35:34 +0200 Subject: [PATCH] Implement out of order streaming with Declarative Shadow DOM --- README.md | 4 +- src/main.rs | 106 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e7f1820..75a3aef 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Streaming HTML -An example of how to out-of-order stream HTML in Rust using as few dependencies as possible. +An example of how to out of HTML out of order streaming in Rust without any JavaScript using as few dependencies as possible. +This is made possible by using declarative shadow DOM. +For a more in depth explanation of how this works, check out this [blog post](https://lamplightdev.com/blog/2024/01/10/streaming-html-out-of-order-without-javascript/) by @lamplightdev. diff --git a/src/main.rs b/src/main.rs index 7c14615..36bb8fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ use std::convert::Infallible; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{Sender, UnboundedSender}; use axum::{ response::{Html, IntoResponse}, routing::get, Router, }; -use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::wrappers::{ReceiverStream, UnboundedReceiverStream}; #[tokio::main] async fn main() { @@ -22,27 +22,97 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } -async fn streamer(sender: Sender>) { - let mut counter = 0; - loop { - counter += 1; - sender - .send(Ok(format!("Hello, World! {}", counter))) - .await - .unwrap(); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - if counter == 10 { - break; - } - } +async fn streamer(sender: UnboundedSender>) { + // Write initial template + // Notes on not obvious things: + // - The template element has to nested within another element to attach the shadow root to + // - The elements to slot have to be siblings of the template element + // - Sibling elements next to the template will not get rendered + // Meaning only elements to be slotted should be placed as siblings to the template + const TEMPLATE: &str = r#" + + + + + + + Document + + +
+ + + "#; + + sender.send(Ok(TEMPLATE.to_string())).unwrap(); + // Showcase out of order + let slot = |slot: u8| format!("Slot {}", slot, slot); + + let next = slot(3); + sender.send(Ok(next)).unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let next = slot(1); + sender.send(Ok(next)).unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let next = slot(4); + sender.send(Ok(next)).unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let next = slot(2); + sender.send(Ok(next)).unwrap(); + + // Close the document + sender + .send(Ok("
".to_string())) + .unwrap(); } async fn handler() -> impl IntoResponse { - let (sender, receiver) = tokio::sync::mpsc::channel(8); + // We know the response is max size will be the HTML document size + // so we can use an unbounded channel + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); tokio::spawn(streamer(sender)); - // let stream = ReceiverStream::from(receiver); - let stream = ReceiverStream::new(receiver); + let stream = UnboundedReceiverStream::new(receiver); let body = axum::body::Body::from_stream(stream); Html(body) } -- 2.51.2