rama_http/service/web/endpoint/response/
partial_updates.rs1use rama_core::bytes::Bytes;
18use rama_core::error::BoxError;
19use rama_core::futures::FutureExt;
20use rama_core::futures::future::BoxFuture;
21use rama_core::futures::stream::{self, StreamExt};
22use rama_http_headers::ContentType;
23use rama_http_types::{Body, Response};
24
25use crate::protocols::html::{IntoHtml, template};
26
27use super::{Headers, IntoResponse};
28
29#[must_use]
36pub struct PartialUpdates<H> {
37 shell: H,
38 fragments: Vec<Fragment>,
39}
40
41type FragmentRender = Box<dyn FnOnce(&mut String) + Send + 'static>;
47
48struct Fragment {
49 name: &'static str,
50 future: BoxFuture<'static, Result<FragmentRender, BoxError>>,
51}
52
53impl<H: IntoHtml> PartialUpdates<H> {
54 pub fn new(shell: H) -> Self {
56 Self {
57 shell,
58 fragments: Vec::new(),
59 }
60 }
61
62 pub fn fragment<F, T>(mut self, name: &'static str, fut: F) -> Self
71 where
72 F: Future<Output = T> + Send + 'static,
73 T: IntoHtml + Send + 'static,
74 {
75 self.fragments.push(Fragment {
76 name,
77 future: async move {
78 let value = fut.await;
79 let render: FragmentRender = Box::new(move |buf| value.escape_and_write(buf));
80 Ok(render)
81 }
82 .boxed(),
83 });
84 self
85 }
86
87 pub fn try_fragment<F, T, E>(mut self, name: &'static str, fut: F) -> Self
90 where
91 F: Future<Output = Result<T, E>> + Send + 'static,
92 T: IntoHtml + Send + 'static,
93 E: Into<BoxError> + Send + 'static,
94 {
95 self.fragments.push(Fragment {
96 name,
97 future: async move {
98 let value = fut.await.map_err(Into::into)?;
99 let render: FragmentRender = Box::new(move |buf| value.escape_and_write(buf));
100 Ok(render)
101 }
102 .boxed(),
103 });
104 self
105 }
106}
107
108impl<H> IntoResponse for PartialUpdates<H>
109where
110 H: IntoHtml + Send + 'static,
111{
112 fn into_response(self) -> Response {
113 let shell = self.shell.into_string();
114 let shell_chunk = stream::once(async move { Ok::<_, BoxError>(Bytes::from(shell)) });
115
116 let frag_count = self.fragments.len().max(1);
117 let fragment_chunks = stream::iter(self.fragments)
118 .map(|Fragment { name, future }| async move {
119 let render = future.await?;
120 let mut s = template!(r#for = name, render).into_string();
121 s.push_str("\n<wbr>");
122 Ok::<_, BoxError>(Bytes::from(s))
123 })
124 .buffer_unordered(frag_count);
125
126 (
127 Headers::single(ContentType::html_utf8()),
128 Body::from_stream(shell_chunk.chain(fragment_chunks)),
129 )
130 .into_response()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::protocols::html::{html, marker, p};
138 use std::time::Duration;
139 use tokio::time::{Instant, sleep};
140
141 #[tokio::test(start_paused = true)]
142 async fn shell_arrives_before_slowest_fragment() {
143 let shell = html!(p!(marker("slow")));
144 let res = PartialUpdates::new(shell)
145 .fragment("slow", async {
146 sleep(Duration::from_millis(500)).await;
147 "ok"
148 })
149 .into_response();
150
151 let mut body = res.into_body();
152 let t0 = Instant::now();
153 let first = body.chunk().await.unwrap().unwrap();
154 let t_first = t0.elapsed();
155
156 assert!(
157 t_first < Duration::from_millis(50),
158 "shell should flush immediately, got {t_first:?}"
159 );
160 let first = std::str::from_utf8(&first).unwrap();
161 assert!(first.contains(r#"<?marker name="slow">"#));
162 assert!(!first.contains("<template for="));
163
164 let second = body.chunk().await.unwrap().unwrap();
165 let t_second = t0.elapsed();
166 assert!(
167 t_second >= Duration::from_millis(500),
168 "fragment should wait for its delay, got {t_second:?}"
169 );
170 assert_eq!(
171 std::str::from_utf8(&second).unwrap(),
172 "<template for=\"slow\">ok</template>\n<wbr>"
173 );
174
175 assert!(body.chunk().await.unwrap().is_none());
176 }
177
178 #[tokio::test(start_paused = true)]
179 async fn fragment_macro_node_keeps_tags() {
180 let shell = html!(p!(marker("x")));
183 let res = PartialUpdates::new(shell)
184 .fragment("x", async { p!("<not-a-tag>") })
185 .into_response();
186 let mut body = res.into_body();
187 let _shell = body.chunk().await.unwrap().unwrap();
188 let tpl = body.chunk().await.unwrap().unwrap();
189 assert_eq!(
190 std::str::from_utf8(&tpl).unwrap(),
191 "<template for=\"x\"><p><not-a-tag></p></template>\n<wbr>"
192 );
193 }
194
195 #[tokio::test(start_paused = true)]
196 async fn fragment_name_is_html_escaped() {
197 let shell = html!(p!(marker("a<b")));
198 let res = PartialUpdates::new(shell)
199 .fragment("a<b", async { "ok" })
200 .into_response();
201 let mut body = res.into_body();
202 let _shell = body.chunk().await.unwrap().unwrap();
203 let tpl = body.chunk().await.unwrap().unwrap();
204 assert_eq!(
205 std::str::from_utf8(&tpl).unwrap(),
206 "<template for=\"a<b\">ok</template>\n<wbr>"
207 );
208 }
209
210 #[tokio::test(start_paused = true)]
211 async fn fragments_stream_in_completion_order() {
212 let shell = html!(p!(marker("a"), marker("b"), marker("c")));
213 let res = PartialUpdates::new(shell)
214 .fragment("a", async {
215 sleep(Duration::from_millis(600)).await;
216 "A"
217 })
218 .fragment("b", async {
219 sleep(Duration::from_millis(100)).await;
220 "B"
221 })
222 .fragment("c", async {
223 sleep(Duration::from_millis(300)).await;
224 "C"
225 })
226 .into_response();
227
228 let mut body = res.into_body();
229 let t0 = Instant::now();
230
231 let mut chunks: Vec<(Duration, String)> = Vec::new();
232 while let Some(chunk) = body.chunk().await.unwrap() {
233 chunks.push((
234 t0.elapsed(),
235 String::from_utf8(chunk.to_vec()).expect("utf8"),
236 ));
237 }
238
239 assert_eq!(chunks.len(), 4, "shell + 3 fragments");
240 assert!(chunks[0].1.contains(r#"<?marker name="a">"#));
241 assert_eq!(chunks[1].1, "<template for=\"b\">B</template>\n<wbr>");
242 assert_eq!(chunks[2].1, "<template for=\"c\">C</template>\n<wbr>");
243 assert_eq!(chunks[3].1, "<template for=\"a\">A</template>\n<wbr>");
244
245 let spread = chunks[3].0.checked_sub(chunks[1].0).unwrap();
246 assert!(
247 spread >= Duration::from_millis(400),
248 "fragment chunks must arrive spread out over time, got {spread:?}"
249 );
250 }
251}