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