use webui_protocol::{web_ui_fragment::Fragment, WebUIProtocol};
use super::root::PendingStreamingRoot;
use super::streaming_boundary_error;
use crate::{route_handler, structural_signal_value, HandlerError, Result, WebUIProcessContext};
pub(super) const BOUNDARY_START_PREFIX: &str = "boundary_start:";
pub(super) const BOUNDARY_END_PREFIX: &str = "boundary_end:";
pub(crate) struct StreamingRenderState<'data> {
pub(super) component_reachability: &'data route_handler::ComponentReachabilityIndex,
pub(super) head_marker_emitted: bool,
pub(super) active_boundary: Option<usize>,
pub(super) pending_root: Option<PendingStreamingRoot<'data>>,
pub(super) generated_root_ready: bool,
pub(super) next_boundary_id: usize,
pub(super) next_record_sequence: usize,
pub(super) checkpoint_updatable: bool,
pub(super) bootstrap_sent: bool,
pub(super) body_ended: bool,
pub(super) inventory: Vec<u8>,
pub(super) inventory_delta: Vec<u8>,
pub(super) inventory_hex: String,
pub(super) template_inventory: Vec<u8>,
pub(super) checkpoint_tags: Vec<u32>,
pub(super) checkpoint_walk_roots: Vec<(u32, Option<Box<str>>)>,
pub(super) checkpoint_seen: Vec<u8>,
pub(super) checkpoint_needs_expansion: bool,
pub(super) state_key_scratch: Vec<&'data str>,
pub(super) checkpoint_name_scratch: Vec<&'data str>,
pub(super) template_tag_scratch: Vec<&'data str>,
pub(super) css_href_scratch: Vec<&'data str>,
pub(super) style_spec_scratch: Vec<&'data str>,
pub(super) reachability_stack: Vec<u32>,
pub(super) update_plans: Vec<Option<StateUpdatePlan>>,
}
pub(super) struct StateUpdatePlan {
pub(super) requires_full_state: bool,
pub(super) keys: Vec<Box<str>>,
}
pub(crate) struct StreamingProgress {
pub(super) head_marker_emitted: bool,
pub(super) active_boundary: Option<usize>,
pub(super) generated_root_ready: bool,
pub(super) next_boundary_id: usize,
pub(super) next_record_sequence: usize,
pub(super) checkpoint_updatable: bool,
pub(super) bootstrap_sent: bool,
pub(super) body_ended: bool,
pub(super) inventory: Vec<u8>,
pub(super) inventory_delta: Vec<u8>,
pub(super) inventory_hex: String,
pub(super) template_inventory: Vec<u8>,
pub(super) checkpoint_tags: Vec<u32>,
pub(super) checkpoint_walk_roots: Vec<(u32, Option<Box<str>>)>,
pub(super) checkpoint_seen: Vec<u8>,
pub(super) checkpoint_needs_expansion: bool,
pub(super) reachability_stack: Vec<u32>,
pub(super) update_plans: Vec<Option<StateUpdatePlan>>,
}
impl StreamingProgress {
pub(crate) fn new(component_count: usize) -> Self {
let inventory_bytes = component_count.div_ceil(8);
Self {
head_marker_emitted: false,
active_boundary: None,
generated_root_ready: false,
next_boundary_id: 0,
next_record_sequence: 0,
checkpoint_updatable: false,
bootstrap_sent: false,
body_ended: false,
inventory: vec![0; inventory_bytes],
inventory_delta: vec![0; inventory_bytes],
inventory_hex: String::with_capacity(inventory_bytes * 2),
template_inventory: vec![0; inventory_bytes],
checkpoint_tags: Vec::new(),
checkpoint_walk_roots: Vec::new(),
checkpoint_seen: vec![0; inventory_bytes],
checkpoint_needs_expansion: false,
reachability_stack: Vec::new(),
update_plans: Vec::new(),
}
}
}
impl<'data> StreamingRenderState<'data> {
pub(crate) fn from_progress(
progress: StreamingProgress,
component_reachability: &'data route_handler::ComponentReachabilityIndex,
) -> Self {
Self {
component_reachability,
pending_root: None,
state_key_scratch: Vec::with_capacity(crate::INITIAL_KEY_CAPACITY),
checkpoint_name_scratch: Vec::new(),
template_tag_scratch: Vec::new(),
css_href_scratch: Vec::new(),
style_spec_scratch: Vec::new(),
head_marker_emitted: progress.head_marker_emitted,
active_boundary: progress.active_boundary,
generated_root_ready: progress.generated_root_ready,
next_boundary_id: progress.next_boundary_id,
next_record_sequence: progress.next_record_sequence,
checkpoint_updatable: progress.checkpoint_updatable,
bootstrap_sent: progress.bootstrap_sent,
body_ended: progress.body_ended,
inventory: progress.inventory,
inventory_delta: progress.inventory_delta,
inventory_hex: progress.inventory_hex,
template_inventory: progress.template_inventory,
checkpoint_tags: progress.checkpoint_tags,
checkpoint_walk_roots: progress.checkpoint_walk_roots,
checkpoint_seen: progress.checkpoint_seen,
checkpoint_needs_expansion: progress.checkpoint_needs_expansion,
reachability_stack: progress.reachability_stack,
update_plans: progress.update_plans,
}
}
pub(crate) fn into_progress(self) -> StreamingProgress {
StreamingProgress {
head_marker_emitted: self.head_marker_emitted,
active_boundary: self.active_boundary,
generated_root_ready: self.generated_root_ready,
next_boundary_id: self.next_boundary_id,
next_record_sequence: self.next_record_sequence,
checkpoint_updatable: self.checkpoint_updatable,
bootstrap_sent: self.bootstrap_sent,
body_ended: self.body_ended,
inventory: self.inventory,
inventory_delta: self.inventory_delta,
inventory_hex: self.inventory_hex,
template_inventory: self.template_inventory,
checkpoint_tags: self.checkpoint_tags,
checkpoint_walk_roots: self.checkpoint_walk_roots,
checkpoint_seen: self.checkpoint_seen,
checkpoint_needs_expansion: self.checkpoint_needs_expansion,
reachability_stack: self.reachability_stack,
update_plans: self.update_plans,
}
}
}
pub(crate) fn validate_streaming_head_start(
protocol: &WebUIProtocol,
entry_id: &str,
) -> Result<()> {
let fragments = protocol
.fragments
.get(entry_id)
.ok_or_else(|| HandlerError::MissingFragment(entry_id.to_string()))?;
for fragment in &fragments.fragments {
let Some(Fragment::Signal(signal)) = fragment.fragment.as_ref() else {
continue;
};
let Some(value) = structural_signal_value(signal) else {
continue;
};
if value == "head_start" {
return Ok(());
}
let before = match value {
"head_end" => Some("head_end"),
"body_start" => Some("body_start"),
"body_end" => Some("body_end"),
value if value.starts_with("boundary_start") || value.starts_with("boundary_end") => {
Some("streaming boundary")
}
_ => None,
};
if let Some(before) = before {
return Err(HandlerError::MissingStreamingHeadStart { before });
}
}
Err(HandlerError::MissingStreamingHeadStart {
before: "end of entry fragment",
})
}
pub(super) fn require_streaming_head_start(
context: &WebUIProcessContext<'_, '_, '_>,
before: &'static str,
) -> Result<()> {
if context
.streaming
.as_ref()
.is_some_and(|streaming| streaming.head_marker_emitted)
{
Ok(())
} else {
Err(HandlerError::MissingStreamingHeadStart { before })
}
}
pub(super) fn increment_streaming_boundary_id(
signal: &str,
streaming: &mut StreamingRenderState<'_>,
) -> Result<()> {
streaming.next_boundary_id = streaming.next_boundary_id.checked_add(1).ok_or_else(|| {
streaming_boundary_error(signal, "boundary sequence overflowed the platform limit")
})?;
Ok(())
}
pub(super) fn increment_streaming_record_sequence(
signal: &str,
streaming: &mut StreamingRenderState<'_>,
) -> Result<()> {
streaming.next_record_sequence =
streaming
.next_record_sequence
.checked_add(1)
.ok_or_else(|| {
streaming_boundary_error(signal, "record sequence overflowed the platform limit")
})?;
Ok(())
}