1mod checkpoint;
23mod error;
24mod inventory;
25mod owned;
26mod plan;
27mod root;
28mod session;
29mod state;
30
31use serde_json::Value;
32
33use crate::route_handler::Protocol;
34use crate::{
35 FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
36 WebUIProcessContext,
37};
38
39pub(crate) use error::streaming_boundary_error;
40pub(crate) use inventory::{record_checkpoint_tag, streaming_template_already_sent};
41pub use owned::{BufferSink, SessionOptions, StreamingSession};
42pub(crate) use plan::{PreparedStreamingEntryPlan, StreamingEntryPlan};
43pub(crate) use root::{
44 consume_streaming_component_root, ensure_no_pending_streaming_root,
45 prepare_generated_streaming_root, validate_pending_streaming_root,
46 validate_streaming_root_opening, ComponentHostOrigin,
47};
48pub use session::{BoundaryId, BoundaryMode, StreamingResponse};
49pub(crate) use state::{validate_streaming_head_start, StreamingRenderState};
50
51use crate::write_usize;
52use error::parse_boundary_sequence;
53use root::process_streaming_root_signal;
54use state::{
55 increment_streaming_boundary_id, increment_streaming_record_sequence,
56 require_streaming_head_start, BOUNDARY_END_PREFIX, BOUNDARY_START_PREFIX,
57};
58
59pub(crate) const STREAMING_MARKER: &str = "<meta name=\"webui-streaming\" content=\"1\">";
60
61struct StreamingSink<'w, W: FlushWriter + ?Sized> {
67 transport: &'w mut W,
68}
69
70impl<W: FlushWriter + ?Sized> ResponseWriter for StreamingSink<'_, W> {
71 fn write(&mut self, content: &str) -> Result<()> {
72 self.transport.write(content)
73 }
74
75 fn end(&mut self) -> Result<()> {
76 self.transport.end()
77 }
78
79 fn stream_flush(&mut self) -> Result<()> {
80 self.transport.flush()
81 }
82}
83
84pub(super) fn streaming_state<'a, 'data>(
85 context: &'a mut WebUIProcessContext<'data, '_, '_>,
86) -> Result<&'a mut StreamingRenderState<'data>> {
87 context.streaming.as_deref_mut().ok_or_else(|| {
88 HandlerError::Invariant("streaming signal processed outside a streaming render".to_string())
89 })
90}
91
92pub(super) fn flush_streaming_transport(
93 context: &mut WebUIProcessContext<'_, '_, '_>,
94) -> Result<()> {
95 if context.streaming.is_none() {
96 return Err(HandlerError::Invariant(
97 "streaming flush requested outside a streaming render".to_string(),
98 ));
99 }
100 context.writer.stream_flush()
103}
104
105impl WebUIHandler {
106 pub(crate) fn process_streaming_signal<'data>(
107 &self,
108 value: &'data str,
109 context: &mut WebUIProcessContext<'data, '_, '_>,
110 ) -> Result<bool> {
111 if context
112 .streaming
113 .as_ref()
114 .is_some_and(|streaming| streaming.body_ended)
115 {
116 return Err(streaming_boundary_error(
117 value,
118 "structural signal arrived after the body_end terminal record",
119 ));
120 }
121
122 if value == "head_start" {
123 if context
124 .streaming
125 .as_ref()
126 .is_some_and(|streaming| streaming.head_marker_emitted)
127 {
128 return Err(HandlerError::DuplicateStreamingHeadStart);
129 }
130 context.writer.write(STREAMING_MARKER)?;
131 streaming_state(context)?.head_marker_emitted = true;
132 return Ok(true);
133 }
134
135 if value.starts_with("streaming_root") {
136 process_streaming_root_signal(value, context)?;
141 return Ok(true);
142 }
143 if let Some(raw_sequence) = value.strip_prefix(BOUNDARY_START_PREFIX) {
144 require_streaming_head_start(context, "streaming boundary")?;
145 let sequence = parse_boundary_sequence(value, raw_sequence)?;
146 let streaming = streaming_state(context)?;
147 if let Some(active) = streaming.active_boundary {
148 return Err(streaming_boundary_error(
149 value,
150 &format!("nested boundary {sequence}; boundary {active} is still open"),
151 ));
152 }
153 if sequence != streaming.next_boundary_id {
154 return Err(streaming_boundary_error(
155 value,
156 &format!(
157 "expected boundary sequence {}, received {sequence}",
158 streaming.next_boundary_id
159 ),
160 ));
161 }
162 context.writer.write("<!--wb:")?;
163 write_usize(context.writer, sequence)?;
164 context.writer.write("-->")?;
165 streaming_state(context)?.active_boundary = Some(sequence);
166 return Ok(true);
167 }
168 if value.starts_with("boundary_start") {
169 require_streaming_head_start(context, "streaming boundary")?;
170 return Err(streaming_boundary_error(
171 value,
172 "expected `boundary_start:<decimal sequence>`",
173 ));
174 }
175
176 if let Some(raw_sequence) = value.strip_prefix(BOUNDARY_END_PREFIX) {
177 require_streaming_head_start(context, "streaming boundary")?;
178 let sequence = parse_boundary_sequence(value, raw_sequence)?;
179 let active = streaming_state(context)?.active_boundary.ok_or_else(|| {
180 streaming_boundary_error(value, "boundary end has no matching start")
181 })?;
182 if active != sequence {
183 return Err(streaming_boundary_error(
184 value,
185 &format!("boundary {active} is open, but boundary {sequence} ended"),
186 ));
187 }
188
189 context.writer.write("<!--/wb:")?;
190 write_usize(context.writer, sequence)?;
191 context.writer.write("-->")?;
192 let (record_sequence, updatable) = {
193 let streaming = streaming_state(context)?;
194 (
195 streaming.next_record_sequence,
196 streaming.checkpoint_updatable,
197 )
198 };
199 self.emit_streaming_checkpoint(record_sequence, sequence, updatable, context)?;
200 let streaming = streaming_state(context)?;
201 streaming.active_boundary = None;
202 streaming.checkpoint_updatable = false;
203 increment_streaming_boundary_id(value, streaming)?;
204 increment_streaming_record_sequence(value, streaming)?;
205 return Ok(true);
206 }
207 if value.starts_with("boundary_end") {
208 require_streaming_head_start(context, "streaming boundary")?;
209 return Err(streaming_boundary_error(
210 value,
211 "expected `boundary_end:<decimal sequence>`",
212 ));
213 }
214
215 if value == "body_end" {
216 require_streaming_head_start(context, "body_end")?;
217 let (active_boundary, body_ended, next_record_sequence) = {
218 let streaming = streaming_state(context)?;
219 (
220 streaming.active_boundary,
221 streaming.body_ended,
222 streaming.next_record_sequence,
223 )
224 };
225 if let Some(active) = active_boundary {
226 return Err(streaming_boundary_error(
227 value,
228 &format!("body ended while boundary {active} was still open"),
229 ));
230 }
231 if body_ended {
232 return Err(streaming_boundary_error(value, "duplicate body_end signal"));
233 }
234 if let Some(html) = context.body_inject {
235 context.writer.write(html)?;
236 }
237 if let Some(html) = context.state_inject.body_end {
242 context.writer.write(html)?;
243 }
244
245 self.emit_streaming_terminal(next_record_sequence, context)?;
251 let streaming = streaming_state(context)?;
252 streaming.body_ended = true;
253 increment_streaming_record_sequence(value, streaming)?;
254 context.body_end_emitted = true;
255 return Ok(true);
256 }
257
258 if value == "head_end" {
259 require_streaming_head_start(context, "head_end")?;
260 } else if value == "body_start" {
261 require_streaming_head_start(context, "body_start")?;
262 }
263
264 Ok(false)
265 }
266
267 pub fn render_streaming<'a, W: FlushWriter + ?Sized>(
274 &self,
275 protocol: &'a Protocol,
276 state: &Value,
277 options: &RenderOptions<'a>,
278 writer: &'a mut W,
279 ) -> Result<()> {
280 let mut response = self.stream_response(protocol, options, writer)?;
281 response.write_shell_buffered(state)?;
282 let boundary_count = response.boundary_count();
283 for boundary in 0..boundary_count {
284 let id = BoundaryId::from_index(boundary)?;
285 response.write_boundary(id, state, BoundaryMode::Final)?;
286 }
287 response.finish(state)
288 }
289}