1use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8use std::sync::Arc;
9
10use serde_json::{Number, Value};
11
12use super::error::{boundary_order_error, state_update_type_error};
13use super::state::{
14 increment_state_revision, increment_streaming_record_sequence, overlay_full_state,
15 overlay_full_state_owned, overlay_selected_state, overlay_selected_state_owned,
16 selected_state_snapshot, selected_state_snapshot_owned, StreamingProgress,
17 StreamingRenderState,
18};
19use super::vm::{ContinuationVm, StepGoal};
20use super::StreamingSink;
21use crate::plugin::HandlerPlugin;
22use crate::route_handler::Protocol;
23use crate::{
24 FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
25 WebUIProcessContext,
26};
27
28pub const MAX_CONTINUATION_DEPTH: usize = 256;
30pub const MAX_SPAN_NESTING: usize = 32;
32pub const MAX_BOUNDARY_OCCURRENCES: usize = 512;
34pub(crate) const MAX_UPDATABLE_OCCURRENCES: usize = 128;
41pub const MAX_KEYED_INSTANCES: usize = 512;
43pub(crate) const MAX_FROZEN_STATE_KEYS: usize = 1_024;
45
46#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub struct BoundaryInstanceId(u32);
49
50impl BoundaryInstanceId {
51 #[must_use]
53 pub const fn from_raw(raw: u32) -> Self {
54 Self(raw)
55 }
56
57 #[must_use]
59 pub const fn raw(self) -> u32 {
60 self.0
61 }
62
63 pub(crate) fn index(self) -> Result<usize> {
64 usize::try_from(self.0).map_err(|_| {
65 HandlerError::Invariant("boundary instance ID does not fit usize".to_string())
66 })
67 }
68}
69
70#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
72pub struct SpanInstanceId(u32);
73
74impl SpanInstanceId {
75 #[must_use]
77 pub const fn from_raw(raw: u32) -> Self {
78 Self(raw)
79 }
80
81 #[must_use]
83 pub const fn raw(self) -> u32 {
84 self.0
85 }
86
87 pub(crate) const fn new(raw: u32) -> Self {
88 Self(raw)
89 }
90}
91
92#[derive(Clone, Debug, Eq, Hash, PartialEq)]
94pub enum BoundaryKey {
95 String(String),
97 Number(Number),
99}
100
101impl BoundaryKey {
102 pub(crate) fn diagnostic(&self) -> String {
103 match self {
104 Self::String(value) => {
105 let mut out = String::with_capacity(value.len() + 2);
106 out.push('"');
107 out.push_str(value);
108 out.push('"');
109 out
110 }
111 Self::Number(value) => value.to_string(),
112 }
113 }
114}
115
116pub enum StreamingState<'a> {
122 Borrowed(&'a Value),
124 Owned(Value),
126}
127
128impl<'a, T> From<&'a T> for StreamingState<'a>
129where
130 T: std::borrow::Borrow<Value> + ?Sized,
131{
132 fn from(state: &'a T) -> Self {
133 Self::Borrowed(std::borrow::Borrow::borrow(state))
134 }
135}
136
137impl<'a, T> From<&'a mut T> for StreamingState<'a>
138where
139 T: std::borrow::Borrow<Value> + ?Sized,
140{
141 fn from(state: &'a mut T) -> Self {
142 Self::Borrowed(std::borrow::Borrow::borrow(&*state))
143 }
144}
145
146impl From<Value> for StreamingState<'_> {
147 fn from(state: Value) -> Self {
148 Self::Owned(state)
149 }
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct BoundaryDescriptor {
155 pub instance_id: BoundaryInstanceId,
157 pub declaration_id: u32,
159 pub owner: Arc<str>,
164 pub name: Arc<str>,
168 pub key: Option<BoundaryKey>,
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum BoundaryMode {
175 Final,
177 Updatable,
179}
180
181#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct StreamStatus {
192 pub boundary: Option<BoundaryDescriptor>,
194 pub done: bool,
196}
197
198pub struct StreamingResponse<'a, W: FlushWriter + ?Sized> {
206 handler: &'a WebUIHandler,
207 protocol: &'a Protocol,
208 options: RenderOptions<'a>,
209 sink: StreamingSink<'a, W>,
210 pub(crate) core: SessionCore,
211}
212
213impl WebUIHandler {
214 pub fn stream_response<'a, W: FlushWriter + ?Sized>(
216 &'a self,
217 protocol: &'a Protocol,
218 options: &RenderOptions<'a>,
219 writer: &'a mut W,
220 ) -> Result<StreamingResponse<'a, W>> {
221 protocol.ensure_style_metadata()?;
222 let core = SessionCore::new(self, protocol, options.entry_id)?;
223 Ok(StreamingResponse {
224 handler: self,
225 protocol,
226 options: RenderOptions {
227 entry_id: options.entry_id,
228 request_path: options.request_path,
229 nonce: options.nonce,
230 head_inject: options.head_inject,
231 body_inject: options.body_inject,
232 },
233 sink: StreamingSink {
234 transport: writer,
235 component_opening: None,
236 written: 0,
237 flushed: 0,
238 },
239 core,
240 })
241 }
242}
243
244impl<W: FlushWriter + ?Sized> StreamingResponse<'_, W> {
245 pub fn start<'state>(
250 &mut self,
251 state: impl Into<StreamingState<'state>>,
252 ) -> Result<StreamStatus> {
253 let (core, call) = self.parts();
254 match state.into() {
255 StreamingState::Borrowed(state) => core.start(call, state),
256 StreamingState::Owned(state) => core.start_owned(call, state),
257 }
258 }
259
260 pub fn resume<'state>(
273 &mut self,
274 instance_id: BoundaryInstanceId,
275 state: impl Into<StreamingState<'state>>,
276 mode: BoundaryMode,
277 ) -> Result<StreamStatus> {
278 let (core, call) = self.parts();
279 match state.into() {
280 StreamingState::Borrowed(state) => core.resume(call, instance_id, state, mode),
281 StreamingState::Owned(state) => core.resume_owned(call, instance_id, state, mode),
282 }
283 }
284
285 pub fn resume_current(
290 &mut self,
291 instance_id: BoundaryInstanceId,
292 mode: BoundaryMode,
293 ) -> Result<StreamStatus> {
294 let (core, call) = self.parts();
295 core.resume_current(call, instance_id, mode)
296 }
297
298 pub fn advance(&mut self) -> Result<StreamStatus> {
303 let (core, call) = self.parts();
304 core.advance(call)
305 }
306
307 pub fn update(&mut self, instance_id: BoundaryInstanceId, patch: &Value) -> Result<()> {
313 let (core, call) = self.parts();
314 core.update(call, instance_id, patch)
315 }
316
317 pub(crate) fn render_all(&mut self, state: &Value) -> Result<()> {
318 let (core, call) = self.parts();
319 core.render_all(call, state)
320 }
321
322 #[must_use]
324 pub fn is_done(&self) -> bool {
325 self.core.done
326 }
327
328 fn parts(&mut self) -> (&mut SessionCore, SessionCall<'_, '_>) {
331 let Self {
332 handler,
333 protocol,
334 options,
335 sink,
336 core,
337 } = self;
338 (
339 core,
340 SessionCall {
341 handler,
342 protocol,
343 options,
344 writer: sink,
345 },
346 )
347 }
348}
349
350pub(crate) struct SessionCore {
351 pub(crate) vm: ContinuationVm,
352 frozen_keys: Arc<[Box<str>]>,
353 requires_full_state: bool,
354 frozen_state: Value,
355 started: bool,
356 pub(crate) done: bool,
357 failed: bool,
358 awaiting_advance: bool,
361 local_vars: HashMap<String, Value>,
362 component_attrs: HashMap<String, Value>,
363 route_base: Option<String>,
364 rendered_components: HashSet<String>,
365 document_style_resources: HashSet<String>,
366 shadow_style_roots: Vec<crate::ShadowStyleRoot>,
367 plugin: Option<Box<dyn HandlerPlugin>>,
368 route_children: crate::RouteChildren<'static>,
369 head_end_emitted: bool,
370 body_start_emitted: bool,
371 component_asset_styles_emitted: bool,
372 body_end_emitted: bool,
373 route_chain_index: usize,
374 route_chain: Option<Vec<crate::route_handler::RouteChainEntry>>,
375 route_document_style_targets: Vec<bool>,
376 reachable_components: Option<Vec<String>>,
377 streaming: Option<StreamingProgress>,
378 json_scratch: Vec<u8>,
379 scope_pool: Vec<HashMap<String, Value>>,
380}
381
382pub(crate) struct SessionCall<'call, 'data> {
383 pub(crate) handler: &'call WebUIHandler,
384 pub(crate) protocol: &'data Protocol,
385 pub(crate) options: &'call RenderOptions<'data>,
386 pub(crate) writer: &'call mut dyn ResponseWriter,
387}
388
389impl SessionCore {
390 pub(crate) fn new(handler: &WebUIHandler, protocol: &Protocol, entry_id: &str) -> Result<Self> {
391 if !protocol.protocol().fragments.contains_key(entry_id) {
392 return Err(HandlerError::MissingFragment(entry_id.to_string()));
393 }
394 let state_plan = protocol.continuation_state_plan(entry_id)?.resolve()?;
395 Ok(Self {
396 vm: ContinuationVm::new(entry_id, protocol)?,
397 frozen_keys: Arc::clone(&state_plan.keys),
398 requires_full_state: state_plan.requires_full_state,
399 frozen_state: Value::Object(serde_json::Map::new()),
400 started: false,
401 done: false,
402 failed: false,
403 awaiting_advance: false,
404 local_vars: HashMap::new(),
405 component_attrs: HashMap::new(),
406 route_base: None,
407 rendered_components: HashSet::new(),
408 document_style_resources: HashSet::new(),
409 shadow_style_roots: Vec::new(),
410 plugin: handler.plugin_factory.map(|factory| factory()),
411 route_children: Cow::Owned(Vec::new()),
412 head_end_emitted: false,
413 body_start_emitted: false,
414 component_asset_styles_emitted: false,
415 body_end_emitted: false,
416 route_chain_index: 0,
417 route_chain: None,
418 route_document_style_targets: Vec::new(),
419 reachable_components: None,
420 streaming: Some(StreamingProgress::new(
421 protocol.component_index().len(),
422 protocol.style_resource_index().len(),
423 )),
424 json_scratch: Vec::new(),
425 scope_pool: Vec::new(),
426 })
427 }
428
429 pub(crate) fn start(
430 &mut self,
431 call: SessionCall<'_, '_>,
432 state: &Value,
433 ) -> Result<StreamStatus> {
434 self.require_usable("start")?;
435 if self.started {
436 return Err(boundary_order_error(
437 "start",
438 "the streaming response has already started",
439 ));
440 }
441 self.frozen_state = if self.requires_full_state {
444 state.clone()
445 } else {
446 selected_state_snapshot(state, &self.frozen_keys)
447 };
448 self.started = true;
449 self.run_step(call, StepGoal::NextBoundary, None)
450 }
451
452 pub(crate) fn start_owned(
453 &mut self,
454 call: SessionCall<'_, '_>,
455 state: Value,
456 ) -> Result<StreamStatus> {
457 self.require_usable("start")?;
458 if self.started {
459 return Err(boundary_order_error(
460 "start",
461 "the streaming response has already started",
462 ));
463 }
464 self.frozen_state = if self.requires_full_state {
465 state
466 } else {
467 selected_state_snapshot_owned(state, &self.frozen_keys)
468 };
469 self.started = true;
470 self.run_step(call, StepGoal::NextBoundary, None)
471 }
472
473 pub(crate) fn resume(
474 &mut self,
475 call: SessionCall<'_, '_>,
476 instance_id: BoundaryInstanceId,
477 state: &Value,
478 mode: BoundaryMode,
479 ) -> Result<StreamStatus> {
480 self.require_resumable()?;
481 self.vm.validate_resume(instance_id)?;
482 self.vm.validate_resume_mode(mode)?;
483 let changed = if self.requires_full_state {
484 overlay_full_state(&mut self.frozen_state, state)
485 } else {
486 overlay_selected_state(&mut self.frozen_state, state, &self.frozen_keys)
487 };
488 self.record_state_change(changed)?;
489 self.run_step(call, StepGoal::CommitBoundary, Some((instance_id, mode)))
490 }
491
492 pub(crate) fn resume_owned(
493 &mut self,
494 call: SessionCall<'_, '_>,
495 instance_id: BoundaryInstanceId,
496 state: Value,
497 mode: BoundaryMode,
498 ) -> Result<StreamStatus> {
499 self.require_resumable()?;
500 self.vm.validate_resume(instance_id)?;
501 self.vm.validate_resume_mode(mode)?;
502 let changed = if self.requires_full_state {
503 overlay_full_state_owned(&mut self.frozen_state, state)
504 } else {
505 overlay_selected_state_owned(&mut self.frozen_state, state, &self.frozen_keys)
506 };
507 self.record_state_change(changed)?;
508 self.run_step(call, StepGoal::CommitBoundary, Some((instance_id, mode)))
509 }
510
511 pub(crate) fn resume_current(
512 &mut self,
513 call: SessionCall<'_, '_>,
514 instance_id: BoundaryInstanceId,
515 mode: BoundaryMode,
516 ) -> Result<StreamStatus> {
517 self.require_resumable()?;
518 self.vm.validate_resume(instance_id)?;
519 self.vm.validate_resume_mode(mode)?;
520 self.run_step(call, StepGoal::CommitBoundary, Some((instance_id, mode)))
521 }
522
523 pub(crate) fn advance(&mut self, call: SessionCall<'_, '_>) -> Result<StreamStatus> {
524 self.require_advanceable()?;
525 self.run_step(call, StepGoal::NextBoundary, None)
526 }
527
528 pub(crate) fn render_all(&mut self, call: SessionCall<'_, '_>, state: &Value) -> Result<()> {
534 self.require_usable("render_streaming")?;
535 if self.started {
536 return Err(boundary_order_error(
537 "render_streaming",
538 "the streaming response has already started",
539 ));
540 }
541 self.started = true;
542 let handler = call.handler;
543 let protocol = call.protocol;
544 let result = self.with_context(call, state, |vm, context| {
545 let mut status = vm.advance(StepGoal::NextBoundary, handler, protocol, context)?;
546 while !status.done {
547 context.writer.stream_flush()?;
548 let Some(boundary) = status.boundary.as_ref() else {
549 return Err(HandlerError::Invariant(
550 "unfinished streaming step has no pending boundary".to_string(),
551 ));
552 };
553 vm.validate_resume(boundary.instance_id)?;
554 vm.validate_resume_mode(BoundaryMode::Final)?;
555 vm.begin_resume(boundary.instance_id, BoundaryMode::Final, context)?;
556 status = vm.advance(StepGoal::NextBoundary, handler, protocol, context)?;
557 }
558 Ok(())
559 });
560 match result {
561 Ok(()) if !self.shadow_style_roots.is_empty() => {
562 self.failed = true;
563 Err(HandlerError::Invariant(
564 "a Shadow CSS tree escaped its component instance".to_string(),
565 ))
566 }
567 Ok(()) => {
568 self.done = true;
569 Ok(())
570 }
571 Err(error) => {
572 self.failed = true;
573 Err(error)
574 }
575 }
576 }
577
578 pub(crate) fn update(
579 &mut self,
580 call: SessionCall<'_, '_>,
581 instance_id: BoundaryInstanceId,
582 patch: &Value,
583 ) -> Result<()> {
584 self.require_usable("update")?;
585 self.require_started("update")?;
586 if self.done {
587 return Err(boundary_order_error(
588 "update",
589 "the streaming response has already completed",
590 ));
591 }
592 if !patch.is_object() {
593 return Err(state_update_type_error());
594 }
595 let target = self.vm.validate_update(instance_id)?;
596 let handler = call.handler;
597 let result = self.with_context(call, patch, |_, context| {
598 let sequence = super::streaming_state(context)?.next_record_sequence;
599 handler.emit_streaming_state_update(sequence, target, context)?;
600 increment_streaming_record_sequence("update", super::streaming_state(context)?)
601 });
602 if result.is_err() {
603 self.failed = true;
604 }
605 result
606 }
607
608 fn run_step(
609 &mut self,
610 call: SessionCall<'_, '_>,
611 goal: StepGoal,
612 resume: Option<(BoundaryInstanceId, BoundaryMode)>,
613 ) -> Result<StreamStatus> {
614 let state = std::mem::replace(
615 &mut self.frozen_state,
616 Value::Object(serde_json::Map::new()),
617 );
618 let handler = call.handler;
619 let protocol = call.protocol;
620 let result = self.with_context(call, &state, |vm, context| {
621 if let Some((instance_id, mode)) = resume {
622 vm.begin_resume(instance_id, mode, context)?;
623 }
624 let status = vm.advance(goal, handler, protocol, context)?;
625 if !status.done {
626 context.writer.stream_flush()?;
627 }
628 Ok(status)
629 });
630 self.frozen_state = state;
631 match result {
632 Ok(status) => {
633 if status.done && !self.shadow_style_roots.is_empty() {
634 self.failed = true;
635 return Err(HandlerError::Invariant(
636 "a Shadow CSS tree escaped its component instance".to_string(),
637 ));
638 }
639 self.done = status.done;
640 self.awaiting_advance = goal == StepGoal::CommitBoundary && !status.done;
641 Ok(status)
642 }
643 Err(error) => {
644 self.failed = true;
645 Err(error)
646 }
647 }
648 }
649
650 fn record_state_change(&mut self, changed: bool) -> Result<()> {
651 if changed {
652 increment_state_revision(self.streaming.as_mut().ok_or_else(missing_progress_error)?)?;
653 }
654 Ok(())
655 }
656
657 fn with_context<'data, 'state, T>(
658 &mut self,
659 call: SessionCall<'_, 'data>,
660 state: &'state Value,
661 operation: impl for<'output> FnOnce(
662 &mut ContinuationVm,
663 &mut WebUIProcessContext<'data, 'state, 'output>,
664 ) -> Result<T>,
665 ) -> Result<T> {
666 let SessionCall {
667 protocol,
668 options,
669 writer,
670 ..
671 } = call;
672 let component_asset_style_manifest = protocol.component_asset_style_manifest()?;
673 let progress = self.streaming.take().ok_or_else(missing_progress_error)?;
674 let mut streaming =
675 StreamingRenderState::from_progress(progress, protocol.component_reachability());
676 let mut context = WebUIProcessContext {
677 protocol: protocol.protocol(),
678 render_fragments: protocol.render_fragments().resolve(protocol.protocol()),
679 component_asset_style_manifest,
680 component_asset_style_links: protocol.component_asset_style_links(),
681 state,
682 writer,
683 local_vars: std::mem::take(&mut self.local_vars),
684 local_borrowed_vars: super::super::BorrowedScope::default(),
685 loop_vars: Vec::new(),
686 visible_loop_scope: super::super::VisibleLoopScope::EMPTY,
687 component_attrs: std::mem::take(&mut self.component_attrs),
688 component_borrowed_attrs: super::super::BorrowedScope::default(),
689 collecting_component_attrs: false,
690 request_path: options.request_path,
691 route_base: self
692 .route_base
693 .take()
694 .map_or(Cow::Borrowed("/"), Cow::Owned),
695 rendered_components: std::mem::take(&mut self.rendered_components),
696 plugin: self.plugin.take(),
697 route_children: std::mem::take(&mut self.route_children),
698 entry_id: options.entry_id,
699 nonce: options.nonce.filter(|nonce| !nonce.is_empty()),
700 component_index: protocol.component_index(),
701 style_resource_index: protocol.style_resource_index(),
702 style_resources_requiring_escape: protocol.style_resources_requiring_escape(),
703 style_chunk_index: protocol.protocol().style_chunk_index(),
704 css_strategy: protocol.css_strategy(),
705 head_inject: options.head_inject.filter(|html| !html.is_empty()),
706 body_inject: options.body_inject.filter(|html| !html.is_empty()),
707 state_inject: crate::StateInject::resolve(state),
708 head_end_emitted: self.head_end_emitted,
709 body_start_emitted: self.body_start_emitted,
710 component_asset_styles_emitted: self.component_asset_styles_emitted,
711 body_end_emitted: self.body_end_emitted,
712 route_index: protocol.route_index(),
713 route_chain_index: self.route_chain_index,
714 route_chain: std::mem::take(&mut self.route_chain),
715 route_document_style_targets: std::mem::take(&mut self.route_document_style_targets),
716 reachable_components: std::mem::take(&mut self.reachable_components),
717 streaming: Some(&mut streaming),
718 json_scratch: std::mem::take(&mut self.json_scratch),
719 scope_pool: std::mem::take(&mut self.scope_pool),
720 document_style_resources: std::mem::take(&mut self.document_style_resources),
721 shadow_style_roots: std::mem::take(&mut self.shadow_style_roots),
722 borrowed_scope_pool: Vec::new(),
723 };
724 let result = operation(&mut self.vm, &mut context);
725 self.local_vars = std::mem::take(&mut context.local_vars);
726 self.component_attrs = std::mem::take(&mut context.component_attrs);
727 self.route_base = match std::mem::replace(&mut context.route_base, Cow::Borrowed("/")) {
728 Cow::Owned(base) => Some(base),
729 Cow::Borrowed(_) => None,
730 };
731 self.rendered_components = std::mem::take(&mut context.rendered_components);
732 self.plugin = context.plugin.take();
733 self.route_children = Cow::Owned(std::mem::take(&mut context.route_children).into_owned());
734 self.head_end_emitted = context.head_end_emitted;
735 self.body_start_emitted = context.body_start_emitted;
736 self.component_asset_styles_emitted = context.component_asset_styles_emitted;
737 self.body_end_emitted = context.body_end_emitted;
738 self.route_chain_index = context.route_chain_index;
739 self.route_chain = std::mem::take(&mut context.route_chain);
740 self.route_document_style_targets =
741 std::mem::take(&mut context.route_document_style_targets);
742 self.reachable_components = std::mem::take(&mut context.reachable_components);
743 self.json_scratch = std::mem::take(&mut context.json_scratch);
744 self.scope_pool = std::mem::take(&mut context.scope_pool);
745 self.shadow_style_roots = std::mem::take(&mut context.shadow_style_roots);
746 self.document_style_resources = std::mem::take(&mut context.document_style_resources);
747 self.streaming = Some(streaming.into_progress());
748 result
749 }
750
751 fn require_usable(&self, operation: &str) -> Result<()> {
752 if self.failed {
753 return Err(boundary_order_error(
754 operation,
755 "the session is poisoned by a previous render or transport failure; start a new response",
756 ));
757 }
758 Ok(())
759 }
760
761 fn require_started(&self, operation: &str) -> Result<()> {
762 if !self.started {
763 return Err(boundary_order_error(
764 operation,
765 "start must be called before this operation",
766 ));
767 }
768 Ok(())
769 }
770
771 fn require_resumable(&self) -> Result<()> {
777 self.require_usable("resume")?;
778 self.require_started("resume")?;
779 if self.done {
780 return Err(boundary_order_error(
781 "resume",
782 "the streaming response has already completed",
783 ));
784 }
785 if self.awaiting_advance {
786 return Err(boundary_order_error(
787 "resume",
788 "the previously committed boundary has not been advanced past; call advance \
789 before resuming the next occurrence",
790 ));
791 }
792 Ok(())
793 }
794
795 fn require_advanceable(&self) -> Result<()> {
797 self.require_usable("advance")?;
798 self.require_started("advance")?;
799 if self.done {
800 return Err(boundary_order_error(
801 "advance",
802 "the streaming response has already completed",
803 ));
804 }
805 if !self.awaiting_advance {
806 return Err(boundary_order_error(
807 "advance",
808 "there is no committed boundary to advance past; resume the pending occurrence \
809 first",
810 ));
811 }
812 Ok(())
813 }
814}
815
816#[cold]
817#[inline(never)]
818fn missing_progress_error() -> HandlerError {
819 HandlerError::Invariant("streaming progress is unavailable".to_string())
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::{FlushWriter, ResponseWriter};
826 use webui_parser::{ComponentRegistration, HtmlParser};
827 use webui_protocol::{ComponentData, InitialStateStrategy, StateProjectionMode, WebUIProtocol};
828 use webui_test_utils::test_json;
829
830 const ISLAND_TAG: &str = "state-island";
831
832 struct TestSink {
833 output: String,
834 }
835
836 impl ResponseWriter for TestSink {
837 fn write(&mut self, content: &str) -> Result<()> {
838 self.output.push_str(content);
839 Ok(())
840 }
841
842 fn end(&mut self) -> Result<()> {
843 Ok(())
844 }
845 }
846
847 impl FlushWriter for TestSink {
848 fn flush(&mut self) -> Result<()> {
849 Ok(())
850 }
851 }
852
853 #[derive(Clone, Default)]
856 struct SharedSink(std::rc::Rc<std::cell::RefCell<String>>);
857
858 impl ResponseWriter for SharedSink {
859 fn write(&mut self, content: &str) -> Result<()> {
860 self.0.borrow_mut().push_str(content);
861 Ok(())
862 }
863
864 fn end(&mut self) -> Result<()> {
865 Ok(())
866 }
867 }
868
869 impl FlushWriter for SharedSink {
870 fn flush(&mut self) -> Result<()> {
871 Ok(())
872 }
873 }
874
875 fn boundary_protocol(boundaries: usize, hydration_mode: StateProjectionMode) -> Protocol {
878 let mut html = String::from("<!doctype html><html><head></head><body>");
879 for sequence in 0..boundaries {
880 html.push_str("<boundary name=\"b");
881 html.push_str(&sequence.to_string());
882 html.push_str("\"><article><");
883 html.push_str(ISLAND_TAG);
884 html.push_str("></");
885 html.push_str(ISLAND_TAG);
886 html.push_str("></article></boundary>");
887 }
888 html.push_str("</body></html>");
889
890 let mut parser = HtmlParser::new();
891 match parser
892 .component_registry_mut()
893 .register_component(ComponentRegistration::new(
894 ISLAND_TAG,
895 "<button>{{title}}</button>",
896 None,
897 true,
898 )) {
899 Ok(()) => {}
900 Err(error) => panic!("registering the island failed: {error}"),
901 }
902 if let Err(error) = parser.parse("index.html", &html) {
903 panic!("parsing the streaming entry failed: {error}");
904 }
905 let mut document = WebUIProtocol::new(parser.into_fragment_records());
906 document.initial_state_strategy = InitialStateStrategy::Components as i32;
907 document.components.insert(
908 ISLAND_TAG.to_string(),
909 ComponentData {
910 template_json: r#"{"h":"<button></button>","th":1}"#.to_string(),
911 uses_shadow_dom: true,
912 hydration_mode: hydration_mode as i32,
913 hydration_keys: if matches!(hydration_mode, StateProjectionMode::Keys) {
914 vec!["count".to_string(), "title".to_string()]
915 } else {
916 Vec::new()
917 },
918 ..Default::default()
919 },
920 );
921 document.populate_style_closures(&["index.html"]);
922 Protocol::new(document)
923 }
924
925 fn large_state(rows: usize) -> Value {
928 let mut items = Vec::with_capacity(rows);
929 for row in 0..rows {
930 items.push(test_json!({
931 "id": row,
932 "label": format!("row-{row}"),
933 "tags": ["alpha", "beta", "gamma"],
934 }));
935 }
936 test_json!({
937 "count": 42,
938 "title": "large state",
939 "rows": items,
940 })
941 }
942
943 fn rows_address(state: &Value) -> Option<usize> {
946 state
947 .get("rows")
948 .and_then(Value::as_array)
949 .map(|rows| rows.as_ptr().addr())
950 }
951
952 fn options<'a>() -> RenderOptions<'a> {
953 RenderOptions::new("index.html", "/")
954 }
955
956 #[test]
957 fn session_parks_owned_routes_and_retains_scope_pool() -> Result<()> {
958 let protocol = boundary_protocol(2, StateProjectionMode::Keys);
959 let handler = WebUIHandler::new();
960 let state = test_json!({ "count": 3, "title": "pooled" });
961 let render_options = options();
962 let mut sink = TestSink {
963 output: String::new(),
964 };
965 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
966
967 let first = response.start(&state)?;
968 assert!(matches!(response.core.route_children, Cow::Owned(_)));
969 let Some(boundary) = first.boundary else {
970 panic!("the first boundary should suspend");
971 };
972
973 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
974 assert!(matches!(response.core.route_children, Cow::Owned(_)));
975 let pooled_capacity = response
976 .core
977 .scope_pool
978 .first()
979 .map(HashMap::capacity)
980 .unwrap_or_else(|| panic!("the completed component should recycle its scope map"));
981
982 response.advance()?;
983 assert!(matches!(response.core.route_children, Cow::Owned(_)));
984 assert_eq!(
985 response.core.scope_pool.first().map(HashMap::capacity),
986 Some(pooled_capacity),
987 "a suspension step must preserve the scope-map pool"
988 );
989 Ok(())
990 }
991
992 #[test]
993 fn render_streaming_projects_full_state_once_per_response() -> Result<()> {
994 let protocol = boundary_protocol(8, StateProjectionMode::All);
999 let handler = WebUIHandler::new();
1000 let state = large_state(256);
1001 let render_options = options();
1002 let mut sink = TestSink {
1003 output: String::new(),
1004 };
1005 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1006
1007 let mut status = response.start(&state)?;
1008 let snapshot = rows_address(&response.core.frozen_state);
1009 assert!(
1010 snapshot.is_some(),
1011 "a full-state protocol must retain the caller's payload"
1012 );
1013 assert_ne!(
1014 snapshot,
1015 rows_address(&state),
1016 "the response owns its snapshot rather than borrowing the caller's tree"
1017 );
1018
1019 let mut committed = 0usize;
1020 while !status.done {
1021 status = match status.boundary.as_ref() {
1022 Some(boundary) => {
1023 let next =
1024 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
1025 committed += 1;
1026 assert!(
1027 next.boundary.is_none() && !next.done,
1028 "a commit step stops at its checkpoint and waits for advance"
1029 );
1030 assert_eq!(
1031 rows_address(&response.core.frozen_state),
1032 snapshot,
1033 "committing occurrence {committed} must not re-copy the retained snapshot"
1034 );
1035 next
1036 }
1037 None => response.advance()?,
1038 };
1039 }
1040 assert_eq!(committed, 8, "every authored boundary must commit");
1041
1042 let mut helper_sink = TestSink {
1045 output: String::new(),
1046 };
1047 handler.render_streaming(&protocol, &state, &render_options, &mut helper_sink)?;
1048 assert_eq!(
1049 helper_sink.output, sink.output,
1050 "render_streaming must resume against the retained snapshot"
1051 );
1052 Ok(())
1053 }
1054
1055 #[test]
1056 fn fused_helper_matches_split_step_bytes_and_flushes() -> Result<()> {
1057 #[derive(Default)]
1062 struct FlushSink {
1063 output: String,
1064 flushes: Vec<usize>,
1065 }
1066
1067 impl ResponseWriter for FlushSink {
1068 fn write(&mut self, content: &str) -> Result<()> {
1069 self.output.push_str(content);
1070 Ok(())
1071 }
1072
1073 fn end(&mut self) -> Result<()> {
1074 Ok(())
1075 }
1076 }
1077
1078 impl FlushWriter for FlushSink {
1079 fn flush(&mut self) -> Result<()> {
1080 self.flushes.push(self.output.len());
1081 Ok(())
1082 }
1083 }
1084
1085 let protocol = boundary_protocol(4, StateProjectionMode::Keys);
1086 let handler = WebUIHandler::new();
1087 let state = test_json!({ "count": 3, "title": "fused" });
1088 let render_options = options();
1089
1090 let mut split = FlushSink::default();
1091 {
1092 let mut response = handler.stream_response(&protocol, &render_options, &mut split)?;
1093 let mut status = response.start(&state)?;
1094 while !status.done {
1095 status = match status.boundary.as_ref() {
1096 Some(boundary) => {
1097 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?
1098 }
1099 None => response.advance()?,
1100 };
1101 }
1102 }
1103
1104 let mut fused = FlushSink::default();
1105 handler.render_streaming(&protocol, &state, &render_options, &mut fused)?;
1106
1107 assert_eq!(fused.output, split.output, "bytes must match");
1108 assert_eq!(fused.flushes, split.flushes, "flush positions must match");
1109 Ok(())
1110 }
1111
1112 #[test]
1113 fn resume_overlays_new_state_and_reuses_unchanged_subtrees() -> Result<()> {
1114 let protocol = boundary_protocol(2, StateProjectionMode::All);
1118 let handler = WebUIHandler::new();
1119 let state = large_state(64);
1120 let render_options = options();
1121 let mut sink = TestSink {
1122 output: String::new(),
1123 };
1124 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1125
1126 let status = response.start(&state)?;
1127 let snapshot = rows_address(&response.core.frozen_state);
1128 let Some(boundary) = status.boundary.as_ref() else {
1129 panic!("the first occurrence must suspend");
1130 };
1131
1132 let mut next = state.clone();
1133 if let Some(object) = next.as_object_mut() {
1134 object.insert("title".to_string(), Value::String("second".to_string()));
1135 object.remove("count");
1136 }
1137 let status = response.resume(boundary.instance_id, &next, BoundaryMode::Final)?;
1138
1139 assert_eq!(
1140 response.core.frozen_state.get("title"),
1141 Some(&Value::String("second".to_string())),
1142 "a changed key must land in the snapshot"
1143 );
1144 assert_eq!(
1145 response.core.frozen_state.get("count"),
1146 Some(&test_json!(42)),
1147 "an omitted key keeps the value the snapshot already holds"
1148 );
1149 assert_eq!(
1150 rows_address(&response.core.frozen_state),
1151 snapshot,
1152 "an unchanged subtree must not be copied again"
1153 );
1154 assert!(
1155 status.boundary.is_none() && !status.done,
1156 "the commit step stops at its checkpoint"
1157 );
1158 assert!(
1159 response.advance()?.boundary.is_some(),
1160 "the second occurrence follows the parent bytes"
1161 );
1162 Ok(())
1163 }
1164
1165 #[test]
1166 fn semantic_steps_reuse_projection_scratch() -> Result<()> {
1167 let protocol = boundary_protocol(6, StateProjectionMode::Keys);
1171 let handler = WebUIHandler::new();
1172 let state = test_json!({ "count": 1, "title": "scratch", "unused": "x" });
1173 let render_options = options();
1174 let mut sink = TestSink {
1175 output: String::new(),
1176 };
1177 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1178
1179 let mut status = response.start(&state)?;
1180 let mut retained: Option<(usize, usize)> = None;
1181 while !status.done {
1182 let Some(boundary) = status.boundary.as_ref() else {
1183 status = response.advance()?;
1184 continue;
1185 };
1186 status = response.resume(boundary.instance_id, &state, BoundaryMode::Updatable)?;
1187 let Some(progress) = response.core.streaming.as_ref() else {
1188 panic!("a suspended response must retain its progress");
1189 };
1190 let observed = (
1191 progress.state_key_ids.capacity(),
1192 progress.state_key_ids.as_ptr().addr(),
1193 );
1194 assert!(
1195 observed.0 > 0,
1196 "the projection scratch must survive the record that filled it"
1197 );
1198 match retained {
1199 None => retained = Some(observed),
1200 Some(previous) => assert_eq!(
1201 observed, previous,
1202 "a later step must reuse the projection buffer, not allocate a new one"
1203 ),
1204 }
1205 }
1206 Ok(())
1207 }
1208
1209 #[test]
1210 fn updates_reuse_their_committed_projection_buffer() -> Result<()> {
1211 let protocol = boundary_protocol(2, StateProjectionMode::Keys);
1215 let handler = WebUIHandler::new();
1216 let state = test_json!({ "count": 1, "title": "updates" });
1217 let render_options = options();
1218 let mut sink = TestSink {
1219 output: String::new(),
1220 };
1221 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1222
1223 let status = response.start(&state)?;
1224 let Some(boundary) = status.boundary.as_ref() else {
1225 panic!("the first occurrence must suspend");
1226 };
1227 let instance_id = boundary.instance_id;
1228 response.resume(instance_id, &state, BoundaryMode::Updatable)?;
1229
1230 let mut retained: Option<(usize, usize)> = None;
1231 for _ in 0..4 {
1232 response.update(instance_id, &state)?;
1233 let Some(progress) = response.core.streaming.as_ref() else {
1234 panic!("a live response must retain its progress");
1235 };
1236 let Some(Some(plan)) = progress.update_plans.first() else {
1237 panic!("an updatable occurrence must retain its projection plan");
1238 };
1239 let observed = (plan.key_ids.capacity(), plan.key_ids.as_ptr().addr());
1240 assert!(!plan.requires_full_state, "keyed islands project keys");
1241 assert!(observed.0 > 0, "the plan must retain its key buffer");
1242 match retained {
1243 None => retained = Some(observed),
1244 Some(previous) => assert_eq!(
1245 observed, previous,
1246 "every update must reuse the committed projection buffer"
1247 ),
1248 }
1249 }
1250 assert_eq!(
1251 sink.output.matches(",2,0,{").count(),
1252 4,
1253 "each update emits exactly one typed state-update record"
1254 );
1255 Ok(())
1256 }
1257
1258 fn commit_occurrences<W: FlushWriter + ?Sized>(
1261 response: &mut StreamingResponse<'_, W>,
1262 status: StreamStatus,
1263 state: &Value,
1264 count: usize,
1265 mode: BoundaryMode,
1266 ) -> Result<StreamStatus> {
1267 let mut status = status;
1268 for committed in 0..count {
1269 let Some(boundary) = status.boundary.as_ref() else {
1270 panic!("occurrence {committed} must suspend before it can commit");
1271 };
1272 let instance_id = boundary.instance_id;
1273 response.resume(instance_id, state, mode)?;
1274 status = response.advance()?;
1275 }
1276 Ok(status)
1277 }
1278
1279 #[test]
1280 fn updatable_commits_stop_at_the_browser_retention_cap() -> Result<()> {
1281 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 1, StateProjectionMode::Keys);
1287 let handler = WebUIHandler::new();
1288 let state = test_json!({ "count": 1, "title": "retained" });
1289 let render_options = options();
1290 let mut sink = SharedSink::default();
1291 let written = SharedSink::clone(&sink).0;
1292 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1293
1294 let status = response.start(&state)?;
1295 let status = commit_occurrences(
1296 &mut response,
1297 status,
1298 &state,
1299 MAX_UPDATABLE_OCCURRENCES,
1300 BoundaryMode::Updatable,
1301 )?;
1302
1303 let Some(boundary) = status.boundary.as_ref() else {
1304 panic!("the occurrence past the cap must suspend like any other");
1305 };
1306 let instance_id = boundary.instance_id;
1307 let bytes_before = written.borrow().len();
1308 let refused = test_json!({ "count": 2, "title": "refused" });
1309 let rejected = response.resume(instance_id, &refused, BoundaryMode::Updatable);
1310 match rejected {
1311 Err(HandlerError::StreamingBoundary(error)) => {
1312 assert_eq!(error.signal, "resume");
1313 assert!(
1314 error.reason.contains("BoundaryMode::Final"),
1315 "the refusal must name the recovery: {}",
1316 error.reason
1317 );
1318 }
1319 _ => panic!("committing past the cap must fail with a typed boundary error"),
1320 }
1321 assert_eq!(
1322 written.borrow().len(),
1323 bytes_before,
1324 "a refused commit must not write a byte"
1325 );
1326 assert_eq!(
1327 response.core.frozen_state.get("title"),
1328 Some(&Value::String("retained".to_string())),
1329 "a refused commit must not merge its state into the snapshot"
1330 );
1331
1332 let status = response.resume(instance_id, &state, BoundaryMode::Final)?;
1334 assert!(
1335 status.boundary.is_none() && !status.done,
1336 "the retried commit stops at its own checkpoint"
1337 );
1338 assert!(
1339 written.borrow().len() > bytes_before,
1340 "the retry writes its checkpoint"
1341 );
1342 assert!(
1343 response.advance()?.done,
1344 "the response still reaches its terminal"
1345 );
1346 Ok(())
1347 }
1348
1349 #[test]
1350 fn final_commits_do_not_consume_the_updatable_cap() -> Result<()> {
1351 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 2, StateProjectionMode::Keys);
1355 let handler = WebUIHandler::new();
1356 let state = test_json!({ "count": 1, "title": "mixed" });
1357 let render_options = options();
1358 let mut sink = TestSink {
1359 output: String::new(),
1360 };
1361 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1362
1363 let status = response.start(&state)?;
1364 let status = commit_occurrences(&mut response, status, &state, 2, BoundaryMode::Final)?;
1365 let status = commit_occurrences(
1366 &mut response,
1367 status,
1368 &state,
1369 MAX_UPDATABLE_OCCURRENCES,
1370 BoundaryMode::Updatable,
1371 )?;
1372 assert!(
1373 status.done && response.is_done(),
1374 "final commits must leave the whole updatable budget available"
1375 );
1376 Ok(())
1377 }
1378
1379 #[test]
1380 fn owned_sessions_enforce_the_same_updatable_cap() -> Result<()> {
1381 let protocol = Arc::new(boundary_protocol(
1384 MAX_UPDATABLE_OCCURRENCES + 1,
1385 StateProjectionMode::Keys,
1386 ));
1387 let mut session = crate::streaming::StreamingSession::new(
1388 Arc::new(WebUIHandler::new()),
1389 protocol,
1390 crate::streaming::SessionOptions::new("index.html", "/"),
1391 )?;
1392 let state = test_json!({ "count": 1, "title": "owned" });
1393
1394 let mut step = session.start(&state)?;
1395 for committed in 0..MAX_UPDATABLE_OCCURRENCES {
1396 let Some(boundary) = step.boundary.as_ref() else {
1397 panic!("occurrence {committed} must suspend before it can commit");
1398 };
1399 let instance_id = boundary.instance_id;
1400 session.resume(instance_id, &state, BoundaryMode::Updatable)?;
1401 step = session.advance()?;
1402 }
1403
1404 let Some(boundary) = step.boundary.as_ref() else {
1405 panic!("the occurrence past the cap must suspend like any other");
1406 };
1407 let instance_id = boundary.instance_id;
1408 assert!(
1409 session
1410 .resume(instance_id, &state, BoundaryMode::Updatable)
1411 .is_err(),
1412 "an owned session refuses the occurrence past the cap"
1413 );
1414 let step = session.resume(instance_id, &state, BoundaryMode::Final)?;
1415 assert!(
1416 !step.bytes.is_empty(),
1417 "the retried commit still delivers its checkpoint bytes"
1418 );
1419 assert!(
1420 session.advance()?.done,
1421 "the owned response still reaches its terminal"
1422 );
1423 Ok(())
1424 }
1425}