use super::inventory::{
commit_checkpoint_inventory, expand_static_checkpoint_reachability,
mark_streaming_template_sent, replace_checkpoint_reachability,
};
use super::{flush_streaming_transport, streaming_state};
use crate::plugin::WebUiTemplatePayload;
use crate::{
collect_hydration_state_into, write_selected_state, write_usize, write_webui_bootstrap, Result,
StateSelection, WebUIHandler, WebUIProcessContext, WebUiBootstrap,
};
use super::state::StateUpdatePlan;
pub(super) const RECORD_KIND_FINAL_CHECKPOINT: usize = 0;
pub(super) const RECORD_KIND_UPDATABLE_CHECKPOINT: usize = 1;
pub(super) const RECORD_KIND_STATE_UPDATE: usize = 2;
pub(super) const RECORD_KIND_TERMINAL: usize = 3;
impl WebUIHandler {
pub(super) fn emit_streaming_checkpoint(
&self,
record_sequence: usize,
boundary_id: usize,
updatable: bool,
context: &mut WebUIProcessContext,
) -> Result<()> {
let first_checkpoint = context
.streaming
.as_ref()
.is_some_and(|streaming| !streaming.bootstrap_sent);
let component_index = context.component_index;
let needs_route_walk = {
let streaming = streaming_state(context)?;
commit_checkpoint_inventory(streaming)?;
!expand_static_checkpoint_reachability(streaming)?
};
if needs_route_walk {
let protocol = context.protocol;
let request_path = context.request_path;
let route_index = context.route_index;
let reachable = {
let streaming = streaming_state(context)?;
let reachability = streaming.component_reachability;
crate::route_handler::collect_reachable_components_from_roots(
protocol,
&streaming.checkpoint_walk_roots,
reachability,
request_path,
route_index,
)
};
replace_checkpoint_reachability(streaming_state(context)?, component_index, &reachable);
}
let (checkpoint_tags, checkpoint_names) = {
let streaming = streaming_state(context)?;
let reachability = streaming.component_reachability;
let tags = std::mem::take(&mut streaming.checkpoint_tags);
let mut names = std::mem::take(&mut streaming.checkpoint_name_scratch);
names.clear();
names.reserve(tags.len());
for &index in &tags {
let Some(name) = reachability.name(index) else {
return Err(crate::HandlerError::Invariant(
"component reachability name is missing".to_string(),
));
};
names.push(name);
}
(tags, names)
};
let mut new_template_tags = {
let streaming = streaming_state(context)?;
std::mem::take(&mut streaming.template_tag_scratch)
};
new_template_tags.clear();
{
let streaming = streaming_state(context)?;
for (&index, &name) in checkpoint_tags.iter().zip(checkpoint_names.iter()) {
if mark_streaming_template_sent(streaming, index)? {
new_template_tags.push(name);
}
}
}
for &name in &new_template_tags {
if !context.rendered_components.contains(name) {
if let Some(css) = context
.protocol
.components
.get(name)
.map(|component| component.css.as_str())
.filter(|css| !css.is_empty())
{
self.emit_css_module_importmap(name, css, context)?;
}
}
}
let template_payloads = context.plugin.as_ref().and_then(|plugin| {
plugin.collect_template_payloads_slice(context.protocol, &new_template_tags)
});
if template_payloads.is_none() {
if let Some(plugin) = context.plugin.as_ref() {
plugin.emit_templates_slice(
context.protocol,
&new_template_tags,
context.nonce,
context.writer,
)?;
}
}
let mut state_key_scratch = {
let streaming = streaming_state(context)?;
std::mem::take(&mut streaming.state_key_scratch)
};
let requires_full_state = collect_hydration_state_into(
context.protocol,
checkpoint_names.iter().copied(),
&mut state_key_scratch,
);
let chain = if first_checkpoint {
crate::route_handler::collect_route_chain(
context.protocol,
context.entry_id,
context.request_path,
context.route_index,
)
.iter()
.map(crate::route_handler::RouteChainEntry::to_json)
.collect::<Vec<_>>()
} else {
Vec::new()
};
let (mut css_hrefs, mut style_specs) = {
let streaming = streaming_state(context)?;
(
std::mem::take(&mut streaming.css_href_scratch),
std::mem::take(&mut streaming.style_spec_scratch),
)
};
css_hrefs.clear();
style_specs.clear();
let is_link = context.protocol.css_strategy() == webui_protocol::CssStrategy::Link;
for &name in &new_template_tags {
if let Some(component) = context.protocol.components.get(name) {
if is_link && !component.css_href.is_empty() {
css_hrefs.push(component.css_href.as_str());
}
if !component.css.is_empty() {
style_specs.push(name);
}
}
}
let empty_payloads: [WebUiTemplatePayload<'_>; 0] = [];
let payloads = template_payloads.as_deref().unwrap_or(&empty_payloads);
context
.writer
.write("<script type=\"application/json\" data-webui-boundary")?;
if let Some(nonce) = context.nonce {
context.writer.write(" nonce=\"")?;
context.writer.write(nonce)?;
context.writer.write("\"")?;
}
context.writer.write(">[1,")?;
write_usize(context.writer, record_sequence)?;
context.writer.write(",")?;
write_usize(
context.writer,
if updatable {
RECORD_KIND_UPDATABLE_CHECKPOINT
} else {
RECORD_KIND_FINAL_CHECKPOINT
},
)?;
context.writer.write(",")?;
write_usize(context.writer, boundary_id)?;
context.writer.write(",")?;
let inventory = context
.streaming
.as_ref()
.map_or("", |streaming| streaming.inventory_hex.as_str());
{
let state_selection = if requires_full_state {
StateSelection::Full
} else {
StateSelection::BorrowedKeys(&state_key_scratch)
};
write_webui_bootstrap(
context.writer,
&mut context.json_scratch,
WebUiBootstrap {
state: context.state,
state_selection,
chain: &chain,
inventory,
nonce: context.nonce,
css_hrefs: &css_hrefs,
style_specs: &style_specs,
templates: payloads,
},
)?;
}
context.writer.write("]</script>")?;
if let Some(plugin) = context.plugin.as_ref() {
plugin.emit_bootstrap_extension_payloads(payloads, context.nonce, context.writer)?;
}
context.writer.write("<webui-hydrate></webui-hydrate>")?;
flush_streaming_transport(context)?;
if let Some(streaming) = context.streaming.as_mut() {
streaming.bootstrap_sent = true;
if updatable {
let target = boundary_id;
if streaming.update_plans.len() <= target {
streaming.update_plans.resize_with(target + 1, || None);
}
streaming.update_plans[target] = Some(StateUpdatePlan {
requires_full_state,
keys: if requires_full_state {
Vec::new()
} else {
state_key_scratch.iter().copied().map(Box::from).collect()
},
});
}
state_key_scratch.clear();
streaming.state_key_scratch = state_key_scratch;
let mut checkpoint_tags = checkpoint_tags;
checkpoint_tags.clear();
streaming.checkpoint_tags = checkpoint_tags;
let mut checkpoint_names = checkpoint_names;
checkpoint_names.clear();
streaming.checkpoint_name_scratch = checkpoint_names;
streaming.checkpoint_walk_roots.clear();
new_template_tags.clear();
streaming.template_tag_scratch = new_template_tags;
css_hrefs.clear();
streaming.css_href_scratch = css_hrefs;
style_specs.clear();
streaming.style_spec_scratch = style_specs;
streaming.checkpoint_seen.fill(0);
streaming.checkpoint_needs_expansion = false;
}
Ok(())
}
pub(super) fn emit_streaming_terminal(
&self,
record_sequence: usize,
context: &mut WebUIProcessContext,
) -> Result<()> {
context
.writer
.write("<script type=\"application/json\" data-webui-boundary")?;
if let Some(nonce) = context.nonce {
context.writer.write(" nonce=\"")?;
context.writer.write(nonce)?;
context.writer.write("\"")?;
}
context.writer.write(">[1,")?;
write_usize(context.writer, record_sequence)?;
context.writer.write(",")?;
write_usize(context.writer, RECORD_KIND_TERMINAL)?;
context
.writer
.write(",0,{}]</script><webui-hydrate></webui-hydrate>")?;
flush_streaming_transport(context)
}
pub(super) fn emit_streaming_state_update(
&self,
record_sequence: usize,
boundary_id: usize,
context: &mut WebUIProcessContext,
) -> Result<()> {
if !context.state.is_object() {
return Err(super::error::state_update_type_error());
}
let Some(plan) = context
.streaming
.as_ref()
.and_then(|streaming| streaming.update_plans.get(boundary_id))
.and_then(Option::as_ref)
else {
return Err(super::error::boundary_not_updatable_error(boundary_id));
};
context
.writer
.write("<script type=\"application/json\" data-webui-boundary")?;
if let Some(nonce) = context.nonce {
context.writer.write(" nonce=\"")?;
context.writer.write(nonce)?;
context.writer.write("\"")?;
}
context.writer.write(">[1,")?;
write_usize(context.writer, record_sequence)?;
context.writer.write(",")?;
write_usize(context.writer, RECORD_KIND_STATE_UPDATE)?;
context.writer.write(",")?;
write_usize(context.writer, boundary_id)?;
context.writer.write(",")?;
let selection = if plan.requires_full_state {
StateSelection::Full
} else {
StateSelection::Keys(plan.keys.iter().map(Box::as_ref).collect())
};
write_selected_state(
context.writer,
&mut context.json_scratch,
context.state,
&selection,
)?;
context
.writer
.write("]</script><webui-hydrate></webui-hydrate>")?;
flush_streaming_transport(context)
}
}