1use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8
9use serde_json::Value;
10
11use super::error::{
12 boundary_not_updatable_error, boundary_order_error, state_update_type_error,
13 unknown_boundary_name_error,
14};
15use super::state::{increment_streaming_record_sequence, StreamingProgress};
16use super::{
17 flush_streaming_transport, validate_streaming_head_start, StreamingEntryPlan,
18 StreamingRenderState, StreamingSink,
19};
20use crate::plugin::HandlerPlugin;
21use crate::route_handler::Protocol;
22use crate::{
23 FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
24 WebUIProcessContext,
25};
26
27#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
33pub struct BoundaryId(u32);
34
35impl BoundaryId {
36 pub(super) fn from_index(index: usize) -> Result<Self> {
37 u32::try_from(index)
38 .map(Self)
39 .map_err(|_| HandlerError::Invariant("boundary index exceeds u32".to_string()))
40 }
41
42 #[must_use]
48 pub fn from_raw(raw: u32) -> Self {
49 Self(raw)
50 }
51
52 #[must_use]
54 pub fn raw(self) -> u32 {
55 self.0
56 }
57
58 fn index(self) -> usize {
59 self.0 as usize
60 }
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum BoundaryMode {
66 Final,
68 Updatable,
70}
71
72pub struct StreamingResponse<'a, W: FlushWriter + ?Sized> {
78 handler: &'a WebUIHandler,
79 protocol: &'a Protocol,
80 plan: ResponsePlan<'a>,
81 sink: StreamingSink<'a, W>,
82 request_path: &'a str,
83 entry_id: &'a str,
84 nonce: Option<&'a str>,
85 head_inject: Option<&'a str>,
86 body_inject: Option<&'a str>,
87 cursor: usize,
89 next_boundary: usize,
90 shell_written: bool,
91 finished: bool,
92 failed: bool,
93 local_vars: HashMap<String, Value>,
94 component_attrs: HashMap<String, Value>,
95 route_base: Cow<'a, str>,
96 rendered_components: HashSet<String>,
97 plugin: Option<Box<dyn HandlerPlugin>>,
98 route_children: Vec<webui_protocol::WebUiFragmentRoute>,
99 head_end_emitted: bool,
100 body_start_emitted: bool,
101 body_end_emitted: bool,
102 route_chain_index: usize,
103 entry_route: Option<(String, crate::route_matcher::RouteMatch)>,
104 streaming: StreamingRenderState<'a>,
105 json_scratch: Vec<u8>,
106 scope_pool: Vec<HashMap<String, Value>>,
107}
108
109enum ResponsePlan<'a> {
110 Shared(&'a StreamingEntryPlan),
111 Request(StreamingEntryPlan),
112}
113
114impl ResponsePlan<'_> {
115 fn get(&self) -> &StreamingEntryPlan {
116 match self {
117 Self::Shared(plan) => plan,
118 Self::Request(plan) => plan,
119 }
120 }
121}
122
123pub(super) struct ParkedResponse {
131 cursor: usize,
132 next_boundary: usize,
133 shell_written: bool,
134 finished: bool,
135 failed: bool,
136 local_vars: HashMap<String, Value>,
137 component_attrs: HashMap<String, Value>,
138 route_base: Box<str>,
139 rendered_components: HashSet<String>,
140 plugin: Option<Box<dyn HandlerPlugin>>,
141 route_children: Vec<webui_protocol::WebUiFragmentRoute>,
142 head_end_emitted: bool,
143 body_start_emitted: bool,
144 body_end_emitted: bool,
145 route_chain_index: usize,
146 entry_route: Option<(String, crate::route_matcher::RouteMatch)>,
147 streaming: StreamingProgress,
148 json_scratch: Vec<u8>,
149 scope_pool: Vec<HashMap<String, Value>>,
150 request_plan: Option<StreamingEntryPlan>,
153}
154
155impl<'a, W: FlushWriter + ?Sized> StreamingResponse<'a, W> {
156 pub(super) fn park(self) -> ParkedResponse {
158 ParkedResponse {
159 cursor: self.cursor,
160 next_boundary: self.next_boundary,
161 shell_written: self.shell_written,
162 finished: self.finished,
163 failed: self.failed,
164 local_vars: self.local_vars,
165 component_attrs: self.component_attrs,
166 route_base: self.route_base.into_owned().into_boxed_str(),
167 rendered_components: self.rendered_components,
168 plugin: self.plugin,
169 route_children: self.route_children,
170 head_end_emitted: self.head_end_emitted,
171 body_start_emitted: self.body_start_emitted,
172 body_end_emitted: self.body_end_emitted,
173 route_chain_index: self.route_chain_index,
174 entry_route: self.entry_route,
175 streaming: self.streaming.into_progress(),
176 json_scratch: self.json_scratch,
177 scope_pool: self.scope_pool,
178 request_plan: match self.plan {
179 ResponsePlan::Shared(_) => None,
180 ResponsePlan::Request(plan) => Some(plan),
181 },
182 }
183 }
184
185 pub(super) fn unpark(
187 parked: ParkedResponse,
188 handler: &'a WebUIHandler,
189 protocol: &'a Protocol,
190 options: &RenderOptions<'a>,
191 writer: &'a mut W,
192 ) -> Result<Self> {
193 let plan = match parked.request_plan {
194 Some(plan) => ResponsePlan::Request(plan),
195 None => match protocol.streaming_plan(options.entry_id)? {
196 Some(plan) => ResponsePlan::Shared(plan),
197 None => {
198 return Err(HandlerError::Invariant(
199 "streaming response plan disappeared between calls".to_string(),
200 ))
201 }
202 },
203 };
204 Ok(Self {
205 handler,
206 protocol,
207 plan,
208 sink: StreamingSink { transport: writer },
209 request_path: options.request_path,
210 entry_id: options.entry_id,
211 nonce: options.nonce.filter(|nonce| !nonce.is_empty()),
212 head_inject: options.head_inject.filter(|html| !html.is_empty()),
213 body_inject: options.body_inject.filter(|html| !html.is_empty()),
214 cursor: parked.cursor,
215 next_boundary: parked.next_boundary,
216 shell_written: parked.shell_written,
217 finished: parked.finished,
218 failed: parked.failed,
219 local_vars: parked.local_vars,
220 component_attrs: parked.component_attrs,
221 route_base: Cow::Owned(parked.route_base.into_string()),
222 rendered_components: parked.rendered_components,
223 plugin: parked.plugin,
224 route_children: parked.route_children,
225 head_end_emitted: parked.head_end_emitted,
226 body_start_emitted: parked.body_start_emitted,
227 body_end_emitted: parked.body_end_emitted,
228 route_chain_index: parked.route_chain_index,
229 entry_route: parked.entry_route,
230 streaming: StreamingRenderState::from_progress(
231 parked.streaming,
232 protocol.component_reachability(),
233 ),
234 json_scratch: parked.json_scratch,
235 scope_pool: parked.scope_pool,
236 })
237 }
238}
239
240impl WebUIHandler {
241 pub fn stream_response<'a, W: FlushWriter + ?Sized>(
248 &'a self,
249 protocol: &'a Protocol,
250 options: &RenderOptions<'a>,
251 writer: &'a mut W,
252 ) -> Result<StreamingResponse<'a, W>> {
253 let document = protocol.protocol();
254 let fragments = document
255 .fragments
256 .get(options.entry_id)
257 .ok_or_else(|| HandlerError::MissingFragment(options.entry_id.to_string()))?;
258 validate_streaming_head_start(document, options.entry_id)?;
259 let plan = match protocol.streaming_plan(options.entry_id)? {
260 Some(plan) => ResponsePlan::Shared(plan),
261 None => ResponsePlan::Request(StreamingEntryPlan::new(
262 options.entry_id,
263 &fragments.fragments,
264 None,
265 )?),
266 };
267 let component_count = protocol.component_index().len();
268 let entry_route = crate::route_renderer::find_best_route_match(
269 &fragments.fragments,
270 options.request_path,
271 "/",
272 protocol.route_index(),
273 );
274
275 Ok(StreamingResponse {
276 handler: self,
277 protocol,
278 plan,
279 sink: StreamingSink { transport: writer },
280 request_path: options.request_path,
281 entry_id: options.entry_id,
282 nonce: options.nonce.filter(|nonce| !nonce.is_empty()),
283 head_inject: options.head_inject.filter(|html| !html.is_empty()),
284 body_inject: options.body_inject.filter(|html| !html.is_empty()),
285 cursor: 0,
286 next_boundary: 0,
287 shell_written: false,
288 finished: false,
289 failed: false,
290 local_vars: HashMap::new(),
291 component_attrs: HashMap::new(),
292 route_base: Cow::Borrowed("/"),
293 rendered_components: HashSet::new(),
294 plugin: self.plugin_factory.map(|factory| factory()),
295 route_children: Vec::new(),
296 head_end_emitted: false,
297 body_start_emitted: false,
298 body_end_emitted: false,
299 route_chain_index: 0,
300 entry_route,
301 streaming: StreamingRenderState::from_progress(
302 StreamingProgress::new(component_count),
303 protocol.component_reachability(),
304 ),
305 json_scratch: Vec::new(),
306 scope_pool: Vec::new(),
307 })
308 }
309}
310
311impl<'a, W: FlushWriter + ?Sized> StreamingResponse<'a, W> {
312 pub fn boundary(&self, name: &str) -> Result<BoundaryId> {
319 let names = self.protocol.streaming_boundary_names(self.entry_id);
320 let Some(index) = names.iter().position(|candidate| candidate == name) else {
321 return Err(unknown_boundary_name_error(name, names));
322 };
323 BoundaryId::from_index(index)
324 }
325
326 #[must_use]
328 pub fn boundary_count(&self) -> usize {
329 self.plan.get().boundary_count()
330 }
331
332 pub fn write_shell(&mut self, state: &Value) -> Result<()> {
334 self.write_shell_internal(state, true)
335 }
336
337 pub(super) fn write_shell_buffered(&mut self, state: &Value) -> Result<()> {
338 self.write_shell_internal(state, false)
339 }
340
341 fn write_shell_internal(&mut self, state: &Value, flush: bool) -> Result<()> {
342 self.require_open("write_shell")?;
343 if self.shell_written {
344 return Err(boundary_order_error(
345 "write_shell",
346 "the response shell has already been written",
347 ));
348 }
349 let shell_end = self.plan.get().shell_end();
350 let result = self.run_range(state, 0..shell_end);
351 self.poison_on_error(&result);
352 result?;
353 self.cursor = shell_end;
354 self.shell_written = true;
355 if flush && !self.body_ended() {
356 let result = self.with_context(state, |_handler, context| {
357 flush_streaming_transport(context)
358 });
359 self.poison_on_error(&result);
360 result?;
361 }
362 Ok(())
363 }
364
365 pub fn write_boundary(
367 &mut self,
368 boundary: BoundaryId,
369 state: &Value,
370 mode: BoundaryMode,
371 ) -> Result<()> {
372 self.require_open("write_boundary")?;
373 if !self.shell_written {
374 return Err(boundary_order_error(
375 "write_boundary",
376 "write_shell must be called before the first boundary",
377 ));
378 }
379 let index = boundary.index();
380 if index != self.next_boundary {
381 return Err(boundary_order_error(
382 "write_boundary",
383 "boundaries must be written once in declaration order",
384 ));
385 }
386 let range = self
387 .plan
388 .get()
389 .boundary(index)
390 .ok_or_else(|| boundary_order_error("write_boundary", "boundary ID is out of range"))?
391 .clone();
392 if range.start < self.cursor {
393 return Err(boundary_order_error(
394 "write_boundary",
395 "boundary content has already been written",
396 ));
397 }
398 self.streaming.checkpoint_updatable = mode == BoundaryMode::Updatable;
399 let result = self.run_range(state, self.cursor..range.end);
400 self.poison_on_error(&result);
401 result?;
402 self.cursor = range.end;
403 self.next_boundary += 1;
404 Ok(())
405 }
406
407 pub fn update(&mut self, boundary: BoundaryId, state: &Value) -> Result<()> {
409 self.require_open("update")?;
410 let target = boundary.index();
411 if target >= self.next_boundary {
412 return Err(boundary_order_error(
413 "update",
414 "the target boundary has not committed yet",
415 ));
416 }
417 if !state.is_object() {
418 return Err(state_update_type_error());
419 }
420 if self
421 .streaming
422 .update_plans
423 .get(target)
424 .and_then(Option::as_ref)
425 .is_none()
426 {
427 return Err(boundary_not_updatable_error(target));
428 }
429 let result = self.with_context(state, |handler, context| {
430 let record_sequence = context
431 .streaming
432 .as_ref()
433 .map_or(0, |streaming| streaming.next_record_sequence);
434 handler.emit_streaming_state_update(record_sequence, target, context)?;
435 increment_streaming_record_sequence("state_update", super::streaming_state(context)?)
436 });
437 self.poison_on_error(&result);
438 result
439 }
440
441 pub fn finish(mut self, state: &Value) -> Result<()> {
443 self.ensure_finishable()?;
444 if !self.body_ended() {
445 let fragment_count = self
446 .protocol
447 .protocol()
448 .fragments
449 .get(self.entry_id)
450 .map_or(0, |fragments| fragments.fragments.len());
451 self.run_range(state, self.cursor..fragment_count)?;
452 }
453 if !self.body_ended() {
454 return Err(HandlerError::MissingStreamingBodyEnd);
455 }
456 self.finished = true;
457 self.sink.end()
458 }
459
460 fn require_open(&self, operation: &str) -> Result<()> {
461 self.require_usable(operation)?;
462 if self.finished || self.body_ended() {
463 Err(boundary_order_error(
464 operation,
465 "the streaming response has already finished",
466 ))
467 } else {
468 Ok(())
469 }
470 }
471
472 pub(super) fn ensure_finishable(&self) -> Result<()> {
478 self.require_usable("finish")?;
479 if self.finished {
480 return Err(boundary_order_error(
481 "finish",
482 "the streaming response has already finished",
483 ));
484 }
485 if !self.shell_written {
486 return Err(boundary_order_error(
487 "finish",
488 "write_shell must be called before finish",
489 ));
490 }
491 if self.next_boundary != self.plan.get().boundary_count() {
492 return Err(boundary_order_error(
493 "finish",
494 "every boundary must be committed before finish",
495 ));
496 }
497 Ok(())
498 }
499
500 fn require_usable(&self, operation: &str) -> Result<()> {
501 if self.failed {
502 return Err(boundary_order_error(
503 operation,
504 "the streaming response is unusable after a previous render or transport failure; \
505 start a new response because bytes may already have been sent",
506 ));
507 }
508 Ok(())
509 }
510
511 fn poison_on_error<T>(&mut self, result: &Result<T>) {
512 if result.is_err() {
513 self.failed = true;
514 }
515 }
516
517 fn body_ended(&self) -> bool {
518 self.streaming.body_ended
519 }
520
521 fn run_range(&mut self, state: &Value, range: std::ops::Range<usize>) -> Result<()> {
522 let protocol: &'a Protocol = self.protocol;
523 let fragments = protocol
524 .protocol()
525 .fragments
526 .get(self.entry_id)
527 .ok_or_else(|| HandlerError::MissingFragment(self.entry_id.to_string()))?;
528 let entry_route = self.entry_route.take();
529 let result = self.with_context(state, |handler, context| {
530 handler.process_fragment_range(&fragments.fragments, range, &entry_route, context)
531 });
532 self.entry_route = entry_route;
533 result
534 }
535
536 fn with_context<'state, T>(
537 &mut self,
538 state: &'state Value,
539 operation: impl for<'output> FnOnce(
540 &WebUIHandler,
541 &mut WebUIProcessContext<'a, 'state, 'output>,
542 ) -> Result<T>,
543 ) -> Result<T> {
544 let mut context = WebUIProcessContext {
545 protocol: self.protocol.protocol(),
546 state,
547 writer: &mut self.sink,
548 local_vars: std::mem::take(&mut self.local_vars),
549 component_attrs: std::mem::take(&mut self.component_attrs),
550 request_path: self.request_path,
551 route_base: std::mem::replace(&mut self.route_base, Cow::Borrowed("/")),
552 rendered_components: std::mem::take(&mut self.rendered_components),
553 plugin: self.plugin.take(),
554 route_children: std::mem::take(&mut self.route_children),
555 entry_id: self.entry_id,
556 nonce: self.nonce,
557 component_index: self.protocol.component_index(),
558 head_inject: self.head_inject,
559 body_inject: self.body_inject,
560 state_inject: crate::StateInject::resolve(state),
561 head_end_emitted: self.head_end_emitted,
562 body_start_emitted: self.body_start_emitted,
563 body_end_emitted: self.body_end_emitted,
564 route_index: self.protocol.route_index(),
565 route_chain_index: self.route_chain_index,
566 streaming: Some(&mut self.streaming),
567 json_scratch: std::mem::take(&mut self.json_scratch),
568 scope_pool: std::mem::take(&mut self.scope_pool),
569 };
570
571 let result = operation(self.handler, &mut context);
572 self.local_vars = std::mem::take(&mut context.local_vars);
573 self.component_attrs = std::mem::take(&mut context.component_attrs);
574 self.route_base = std::mem::replace(&mut context.route_base, Cow::Borrowed("/"));
575 self.rendered_components = std::mem::take(&mut context.rendered_components);
576 self.plugin = context.plugin.take();
577 self.route_children = std::mem::take(&mut context.route_children);
578 self.head_end_emitted = context.head_end_emitted;
579 self.body_start_emitted = context.body_start_emitted;
580 self.body_end_emitted = context.body_end_emitted;
581 self.route_chain_index = context.route_chain_index;
582 self.json_scratch = std::mem::take(&mut context.json_scratch);
583 self.scope_pool = std::mem::take(&mut context.scope_pool);
584 result
585 }
586}