mod checkpoint;
mod error;
mod inventory;
mod owned;
mod plan;
mod root;
mod session;
mod state;
use serde_json::Value;
use crate::route_handler::Protocol;
use crate::{
FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
WebUIProcessContext,
};
pub(crate) use error::streaming_boundary_error;
pub(crate) use inventory::{record_checkpoint_tag, streaming_template_already_sent};
pub use owned::{BufferSink, SessionOptions, StreamingSession};
pub(crate) use plan::{PreparedStreamingEntryPlan, StreamingEntryPlan};
pub(crate) use root::{
consume_streaming_component_root, ensure_no_pending_streaming_root,
prepare_generated_streaming_root, validate_pending_streaming_root,
validate_streaming_root_opening, ComponentHostOrigin,
};
pub use session::{BoundaryId, BoundaryMode, StreamingResponse};
pub(crate) use state::{validate_streaming_head_start, StreamingRenderState};
use crate::write_usize;
use error::parse_boundary_sequence;
use root::process_streaming_root_signal;
use state::{
increment_streaming_boundary_id, increment_streaming_record_sequence,
require_streaming_head_start, BOUNDARY_END_PREFIX, BOUNDARY_START_PREFIX,
};
pub(crate) const STREAMING_MARKER: &str = "<meta name=\"webui-streaming\" content=\"1\">";
struct StreamingSink<'w, W: FlushWriter + ?Sized> {
transport: &'w mut W,
}
impl<W: FlushWriter + ?Sized> ResponseWriter for StreamingSink<'_, W> {
fn write(&mut self, content: &str) -> Result<()> {
self.transport.write(content)
}
fn end(&mut self) -> Result<()> {
self.transport.end()
}
fn stream_flush(&mut self) -> Result<()> {
self.transport.flush()
}
}
pub(super) fn streaming_state<'a, 'data>(
context: &'a mut WebUIProcessContext<'data, '_, '_>,
) -> Result<&'a mut StreamingRenderState<'data>> {
context.streaming.as_deref_mut().ok_or_else(|| {
HandlerError::Invariant("streaming signal processed outside a streaming render".to_string())
})
}
pub(super) fn flush_streaming_transport(
context: &mut WebUIProcessContext<'_, '_, '_>,
) -> Result<()> {
if context.streaming.is_none() {
return Err(HandlerError::Invariant(
"streaming flush requested outside a streaming render".to_string(),
));
}
context.writer.stream_flush()
}
impl WebUIHandler {
pub(crate) fn process_streaming_signal<'data>(
&self,
value: &'data str,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<bool> {
if context
.streaming
.as_ref()
.is_some_and(|streaming| streaming.body_ended)
{
return Err(streaming_boundary_error(
value,
"structural signal arrived after the body_end terminal record",
));
}
if value == "head_start" {
if context
.streaming
.as_ref()
.is_some_and(|streaming| streaming.head_marker_emitted)
{
return Err(HandlerError::DuplicateStreamingHeadStart);
}
context.writer.write(STREAMING_MARKER)?;
streaming_state(context)?.head_marker_emitted = true;
return Ok(true);
}
if value.starts_with("streaming_root") {
process_streaming_root_signal(value, context)?;
return Ok(true);
}
if let Some(raw_sequence) = value.strip_prefix(BOUNDARY_START_PREFIX) {
require_streaming_head_start(context, "streaming boundary")?;
let sequence = parse_boundary_sequence(value, raw_sequence)?;
let streaming = streaming_state(context)?;
if let Some(active) = streaming.active_boundary {
return Err(streaming_boundary_error(
value,
&format!("nested boundary {sequence}; boundary {active} is still open"),
));
}
if sequence != streaming.next_boundary_id {
return Err(streaming_boundary_error(
value,
&format!(
"expected boundary sequence {}, received {sequence}",
streaming.next_boundary_id
),
));
}
context.writer.write("<!--wb:")?;
write_usize(context.writer, sequence)?;
context.writer.write("-->")?;
streaming_state(context)?.active_boundary = Some(sequence);
return Ok(true);
}
if value.starts_with("boundary_start") {
require_streaming_head_start(context, "streaming boundary")?;
return Err(streaming_boundary_error(
value,
"expected `boundary_start:<decimal sequence>`",
));
}
if let Some(raw_sequence) = value.strip_prefix(BOUNDARY_END_PREFIX) {
require_streaming_head_start(context, "streaming boundary")?;
let sequence = parse_boundary_sequence(value, raw_sequence)?;
let active = streaming_state(context)?.active_boundary.ok_or_else(|| {
streaming_boundary_error(value, "boundary end has no matching start")
})?;
if active != sequence {
return Err(streaming_boundary_error(
value,
&format!("boundary {active} is open, but boundary {sequence} ended"),
));
}
context.writer.write("<!--/wb:")?;
write_usize(context.writer, sequence)?;
context.writer.write("-->")?;
let (record_sequence, updatable) = {
let streaming = streaming_state(context)?;
(
streaming.next_record_sequence,
streaming.checkpoint_updatable,
)
};
self.emit_streaming_checkpoint(record_sequence, sequence, updatable, context)?;
let streaming = streaming_state(context)?;
streaming.active_boundary = None;
streaming.checkpoint_updatable = false;
increment_streaming_boundary_id(value, streaming)?;
increment_streaming_record_sequence(value, streaming)?;
return Ok(true);
}
if value.starts_with("boundary_end") {
require_streaming_head_start(context, "streaming boundary")?;
return Err(streaming_boundary_error(
value,
"expected `boundary_end:<decimal sequence>`",
));
}
if value == "body_end" {
require_streaming_head_start(context, "body_end")?;
let (active_boundary, body_ended, next_record_sequence) = {
let streaming = streaming_state(context)?;
(
streaming.active_boundary,
streaming.body_ended,
streaming.next_record_sequence,
)
};
if let Some(active) = active_boundary {
return Err(streaming_boundary_error(
value,
&format!("body ended while boundary {active} was still open"),
));
}
if body_ended {
return Err(streaming_boundary_error(value, "duplicate body_end signal"));
}
if let Some(html) = context.body_inject {
context.writer.write(html)?;
}
if let Some(html) = context.state_inject.body_end {
context.writer.write(html)?;
}
self.emit_streaming_terminal(next_record_sequence, context)?;
let streaming = streaming_state(context)?;
streaming.body_ended = true;
increment_streaming_record_sequence(value, streaming)?;
context.body_end_emitted = true;
return Ok(true);
}
if value == "head_end" {
require_streaming_head_start(context, "head_end")?;
} else if value == "body_start" {
require_streaming_head_start(context, "body_start")?;
}
Ok(false)
}
pub fn render_streaming<'a, W: FlushWriter + ?Sized>(
&self,
protocol: &'a Protocol,
state: &Value,
options: &RenderOptions<'a>,
writer: &'a mut W,
) -> Result<()> {
let mut response = self.stream_response(protocol, options, writer)?;
response.write_shell_buffered(state)?;
let boundary_count = response.boundary_count();
for boundary in 0..boundary_count {
let id = BoundaryId::from_index(boundary)?;
response.write_boundary(id, state, BoundaryMode::Final)?;
}
response.finish(state)
}
}