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_streaming_record_sequence, overlay_full_state, overlay_selected_state,
15 selected_state_snapshot, StreamingProgress, StreamingRenderState,
16};
17use super::vm::{ContinuationVm, StepGoal};
18use super::StreamingSink;
19use crate::plugin::HandlerPlugin;
20use crate::route_handler::Protocol;
21use crate::{
22 FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
23 WebUIProcessContext,
24};
25
26pub const MAX_CONTINUATION_DEPTH: usize = 256;
28pub const MAX_SPAN_NESTING: usize = 32;
30pub const MAX_BOUNDARY_OCCURRENCES: usize = 512;
32pub(crate) const MAX_UPDATABLE_OCCURRENCES: usize = 128;
39pub const MAX_KEYED_INSTANCES: usize = 512;
41pub(crate) const MAX_FROZEN_STATE_KEYS: usize = 1_024;
43
44#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
46pub struct BoundaryInstanceId(u32);
47
48impl BoundaryInstanceId {
49 #[must_use]
51 pub const fn from_raw(raw: u32) -> Self {
52 Self(raw)
53 }
54
55 #[must_use]
57 pub const fn raw(self) -> u32 {
58 self.0
59 }
60
61 pub(crate) fn index(self) -> Result<usize> {
62 usize::try_from(self.0).map_err(|_| {
63 HandlerError::Invariant("boundary instance ID does not fit usize".to_string())
64 })
65 }
66}
67
68#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
70pub struct SpanInstanceId(u32);
71
72impl SpanInstanceId {
73 #[must_use]
75 pub const fn from_raw(raw: u32) -> Self {
76 Self(raw)
77 }
78
79 #[must_use]
81 pub const fn raw(self) -> u32 {
82 self.0
83 }
84
85 pub(crate) const fn new(raw: u32) -> Self {
86 Self(raw)
87 }
88}
89
90#[derive(Clone, Debug, Eq, Hash, PartialEq)]
92pub enum BoundaryKey {
93 String(String),
95 Number(Number),
97}
98
99impl BoundaryKey {
100 pub(crate) fn diagnostic(&self) -> String {
101 match self {
102 Self::String(value) => {
103 let mut out = String::with_capacity(value.len() + 2);
104 out.push('"');
105 out.push_str(value);
106 out.push('"');
107 out
108 }
109 Self::Number(value) => value.to_string(),
110 }
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct BoundaryDescriptor {
117 pub instance_id: BoundaryInstanceId,
119 pub declaration_id: u32,
121 pub owner: Arc<str>,
126 pub name: Arc<str>,
130 pub key: Option<BoundaryKey>,
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub enum BoundaryMode {
137 Final,
139 Updatable,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct StreamStatus {
154 pub boundary: Option<BoundaryDescriptor>,
156 pub done: bool,
158}
159
160pub struct StreamingResponse<'a, W: FlushWriter + ?Sized> {
168 handler: &'a WebUIHandler,
169 protocol: &'a Protocol,
170 options: RenderOptions<'a>,
171 sink: StreamingSink<'a, W>,
172 pub(crate) core: SessionCore,
173}
174
175impl WebUIHandler {
176 pub fn stream_response<'a, W: FlushWriter + ?Sized>(
178 &'a self,
179 protocol: &'a Protocol,
180 options: &RenderOptions<'a>,
181 writer: &'a mut W,
182 ) -> Result<StreamingResponse<'a, W>> {
183 protocol.ensure_style_metadata()?;
184 let core = SessionCore::new(self, protocol, options.entry_id)?;
185 Ok(StreamingResponse {
186 handler: self,
187 protocol,
188 options: RenderOptions {
189 entry_id: options.entry_id,
190 request_path: options.request_path,
191 nonce: options.nonce,
192 head_inject: options.head_inject,
193 body_inject: options.body_inject,
194 },
195 sink: StreamingSink {
196 transport: writer,
197 component_opening: None,
198 written: 0,
199 flushed: 0,
200 },
201 core,
202 })
203 }
204}
205
206impl<W: FlushWriter + ?Sized> StreamingResponse<'_, W> {
207 pub fn start(&mut self, state: &Value) -> Result<StreamStatus> {
209 let (core, call) = self.parts();
210 core.start(call, state)
211 }
212
213 pub fn resume(
224 &mut self,
225 instance_id: BoundaryInstanceId,
226 state: &Value,
227 mode: BoundaryMode,
228 ) -> Result<StreamStatus> {
229 let (core, call) = self.parts();
230 core.resume(call, instance_id, state, mode)
231 }
232
233 pub fn advance(&mut self) -> Result<StreamStatus> {
238 let (core, call) = self.parts();
239 core.advance(call)
240 }
241
242 pub fn update(&mut self, instance_id: BoundaryInstanceId, patch: &Value) -> Result<()> {
248 let (core, call) = self.parts();
249 core.update(call, instance_id, patch)
250 }
251
252 pub(crate) fn resume_current_and_advance(
258 &mut self,
259 instance_id: BoundaryInstanceId,
260 mode: BoundaryMode,
261 ) -> Result<StreamStatus> {
262 let (core, call) = self.parts();
263 core.resume_current_and_advance(call, instance_id, mode)
264 }
265
266 #[must_use]
268 pub fn is_done(&self) -> bool {
269 self.core.done
270 }
271
272 fn parts(&mut self) -> (&mut SessionCore, SessionCall<'_, '_>) {
275 let Self {
276 handler,
277 protocol,
278 options,
279 sink,
280 core,
281 } = self;
282 (
283 core,
284 SessionCall {
285 handler,
286 protocol,
287 options,
288 writer: sink,
289 },
290 )
291 }
292}
293
294pub(crate) struct SessionCore {
295 pub(crate) vm: ContinuationVm,
296 frozen_keys: Arc<[Box<str>]>,
297 requires_full_state: bool,
298 frozen_state: Value,
299 started: bool,
300 pub(crate) done: bool,
301 failed: bool,
302 awaiting_advance: bool,
305 local_vars: HashMap<String, Value>,
306 component_attrs: HashMap<String, Value>,
307 route_base: Option<String>,
308 rendered_components: HashSet<String>,
309 document_style_resources: HashSet<String>,
310 shadow_style_roots: Vec<crate::ShadowStyleRoot>,
311 plugin: Option<Box<dyn HandlerPlugin>>,
312 route_children: Vec<webui_protocol::WebUiFragmentRoute>,
313 head_end_emitted: bool,
314 body_start_emitted: bool,
315 component_asset_styles_emitted: bool,
316 body_end_emitted: bool,
317 route_chain_index: usize,
318 route_chain: Option<Vec<crate::route_handler::RouteChainEntry>>,
319 route_document_style_targets: Vec<bool>,
320 reachable_components: Option<Vec<String>>,
321 streaming: Option<StreamingProgress>,
322 json_scratch: Vec<u8>,
323 scope_pool: Vec<HashMap<String, Value>>,
324}
325
326pub(crate) struct SessionCall<'call, 'data> {
327 pub(crate) handler: &'call WebUIHandler,
328 pub(crate) protocol: &'data Protocol,
329 pub(crate) options: &'call RenderOptions<'data>,
330 pub(crate) writer: &'call mut dyn ResponseWriter,
331}
332
333impl SessionCore {
334 pub(crate) fn new(handler: &WebUIHandler, protocol: &Protocol, entry_id: &str) -> Result<Self> {
335 if !protocol.protocol().fragments.contains_key(entry_id) {
336 return Err(HandlerError::MissingFragment(entry_id.to_string()));
337 }
338 let state_plan = protocol.continuation_state_plan(entry_id)?.resolve()?;
339 Ok(Self {
340 vm: ContinuationVm::new(entry_id, protocol)?,
341 frozen_keys: Arc::clone(&state_plan.keys),
342 requires_full_state: state_plan.requires_full_state,
343 frozen_state: Value::Object(serde_json::Map::new()),
344 started: false,
345 done: false,
346 failed: false,
347 awaiting_advance: false,
348 local_vars: HashMap::new(),
349 component_attrs: HashMap::new(),
350 route_base: None,
351 rendered_components: HashSet::new(),
352 document_style_resources: HashSet::new(),
353 shadow_style_roots: Vec::new(),
354 plugin: handler.plugin_factory.map(|factory| factory()),
355 route_children: Vec::new(),
356 head_end_emitted: false,
357 body_start_emitted: false,
358 component_asset_styles_emitted: false,
359 body_end_emitted: false,
360 route_chain_index: 0,
361 route_chain: None,
362 route_document_style_targets: Vec::new(),
363 reachable_components: None,
364 streaming: Some(StreamingProgress::new(
365 protocol.component_index().len(),
366 protocol.style_resource_index().len(),
367 )),
368 json_scratch: Vec::new(),
369 scope_pool: Vec::new(),
370 })
371 }
372
373 pub(crate) fn start(
374 &mut self,
375 call: SessionCall<'_, '_>,
376 state: &Value,
377 ) -> Result<StreamStatus> {
378 self.require_usable("start")?;
379 if self.started {
380 return Err(boundary_order_error(
381 "start",
382 "the streaming response has already started",
383 ));
384 }
385 self.frozen_state = if self.requires_full_state {
388 state.clone()
389 } else {
390 selected_state_snapshot(state, &self.frozen_keys)
391 };
392 self.started = true;
393 self.run_step(call, StepGoal::NextBoundary, None)
394 }
395
396 pub(crate) fn resume(
397 &mut self,
398 call: SessionCall<'_, '_>,
399 instance_id: BoundaryInstanceId,
400 state: &Value,
401 mode: BoundaryMode,
402 ) -> Result<StreamStatus> {
403 self.require_resumable()?;
404 self.vm.validate_resume(instance_id)?;
405 self.vm.validate_resume_mode(mode)?;
406 if self.requires_full_state {
407 overlay_full_state(&mut self.frozen_state, state);
408 } else {
409 overlay_selected_state(&mut self.frozen_state, state, &self.frozen_keys);
410 }
411 self.run_step(call, StepGoal::CommitBoundary, Some((instance_id, mode)))
412 }
413
414 pub(crate) fn advance(&mut self, call: SessionCall<'_, '_>) -> Result<StreamStatus> {
415 self.require_advanceable()?;
416 self.run_step(call, StepGoal::NextBoundary, None)
417 }
418
419 pub(crate) fn resume_current_and_advance(
432 &mut self,
433 call: SessionCall<'_, '_>,
434 instance_id: BoundaryInstanceId,
435 mode: BoundaryMode,
436 ) -> Result<StreamStatus> {
437 self.require_resumable()?;
438 self.vm.validate_resume(instance_id)?;
439 self.vm.validate_resume_mode(mode)?;
440 self.run_step(call, StepGoal::NextBoundary, Some((instance_id, mode)))
441 }
442
443 pub(crate) fn update(
444 &mut self,
445 call: SessionCall<'_, '_>,
446 instance_id: BoundaryInstanceId,
447 patch: &Value,
448 ) -> Result<()> {
449 self.require_usable("update")?;
450 self.require_started("update")?;
451 if self.done {
452 return Err(boundary_order_error(
453 "update",
454 "the streaming response has already completed",
455 ));
456 }
457 if !patch.is_object() {
458 return Err(state_update_type_error());
459 }
460 let target = self.vm.validate_update(instance_id)?;
461 let handler = call.handler;
462 let result = self.with_context(call, patch, |_, context| {
463 let sequence = super::streaming_state(context)?.next_record_sequence;
464 handler.emit_streaming_state_update(sequence, target, context)?;
465 increment_streaming_record_sequence("update", super::streaming_state(context)?)
466 });
467 if result.is_err() {
468 self.failed = true;
469 }
470 result
471 }
472
473 fn run_step(
474 &mut self,
475 call: SessionCall<'_, '_>,
476 goal: StepGoal,
477 resume: Option<(BoundaryInstanceId, BoundaryMode)>,
478 ) -> Result<StreamStatus> {
479 let state = std::mem::replace(
480 &mut self.frozen_state,
481 Value::Object(serde_json::Map::new()),
482 );
483 let handler = call.handler;
484 let protocol = call.protocol;
485 let result = self.with_context(call, &state, |vm, context| {
486 if let Some((instance_id, mode)) = resume {
487 vm.begin_resume(instance_id, mode, context)?;
488 }
489 let status = vm.advance(goal, handler, protocol, context)?;
490 if !status.done {
491 context.writer.stream_flush()?;
492 }
493 Ok(status)
494 });
495 self.frozen_state = state;
496 match result {
497 Ok(status) => {
498 if status.done && !self.shadow_style_roots.is_empty() {
499 self.failed = true;
500 return Err(HandlerError::Invariant(
501 "a Shadow CSS tree escaped its component instance".to_string(),
502 ));
503 }
504 self.done = status.done;
505 self.awaiting_advance = goal == StepGoal::CommitBoundary && !status.done;
506 Ok(status)
507 }
508 Err(error) => {
509 self.failed = true;
510 Err(error)
511 }
512 }
513 }
514
515 fn with_context<'data, 'state, T>(
516 &mut self,
517 call: SessionCall<'_, 'data>,
518 state: &'state Value,
519 operation: impl for<'output> FnOnce(
520 &mut ContinuationVm,
521 &mut WebUIProcessContext<'data, 'state, 'output>,
522 ) -> Result<T>,
523 ) -> Result<T> {
524 let SessionCall {
525 protocol,
526 options,
527 writer,
528 ..
529 } = call;
530 let component_asset_style_manifest = protocol.component_asset_style_manifest()?;
531 let progress = self.streaming.take().ok_or_else(missing_progress_error)?;
532 let mut streaming =
533 StreamingRenderState::from_progress(progress, protocol.component_reachability());
534 let mut context = WebUIProcessContext {
535 protocol: protocol.protocol(),
536 render_fragments: protocol.render_fragments().resolve(protocol.protocol()),
537 component_asset_style_manifest,
538 component_asset_style_links: protocol.component_asset_style_links(),
539 state,
540 writer,
541 local_vars: std::mem::take(&mut self.local_vars),
542 local_borrowed_vars: super::super::BorrowedScope::default(),
543 loop_vars: Vec::new(),
544 visible_loop_scope: super::super::VisibleLoopScope::EMPTY,
545 component_attrs: std::mem::take(&mut self.component_attrs),
546 component_borrowed_attrs: super::super::BorrowedScope::default(),
547 collecting_component_attrs: false,
548 request_path: options.request_path,
549 route_base: self
550 .route_base
551 .take()
552 .map_or(Cow::Borrowed("/"), Cow::Owned),
553 rendered_components: std::mem::take(&mut self.rendered_components),
554 plugin: self.plugin.take(),
555 route_children: std::mem::take(&mut self.route_children),
556 entry_id: options.entry_id,
557 nonce: options.nonce.filter(|nonce| !nonce.is_empty()),
558 component_index: protocol.component_index(),
559 style_resource_index: protocol.style_resource_index(),
560 style_resources_requiring_escape: protocol.style_resources_requiring_escape(),
561 style_chunk_index: protocol.protocol().style_chunk_index(),
562 css_strategy: protocol.css_strategy(),
563 head_inject: options.head_inject.filter(|html| !html.is_empty()),
564 body_inject: options.body_inject.filter(|html| !html.is_empty()),
565 state_inject: crate::StateInject::resolve(state),
566 head_end_emitted: self.head_end_emitted,
567 body_start_emitted: self.body_start_emitted,
568 component_asset_styles_emitted: self.component_asset_styles_emitted,
569 body_end_emitted: self.body_end_emitted,
570 route_index: protocol.route_index(),
571 route_chain_index: self.route_chain_index,
572 route_chain: std::mem::take(&mut self.route_chain),
573 route_document_style_targets: std::mem::take(&mut self.route_document_style_targets),
574 reachable_components: std::mem::take(&mut self.reachable_components),
575 streaming: Some(&mut streaming),
576 json_scratch: std::mem::take(&mut self.json_scratch),
577 scope_pool: std::mem::take(&mut self.scope_pool),
578 document_style_resources: std::mem::take(&mut self.document_style_resources),
579 shadow_style_roots: std::mem::take(&mut self.shadow_style_roots),
580 borrowed_scope_pool: Vec::new(),
581 };
582 let result = operation(&mut self.vm, &mut context);
583 self.local_vars = std::mem::take(&mut context.local_vars);
584 self.component_attrs = std::mem::take(&mut context.component_attrs);
585 self.route_base = match std::mem::replace(&mut context.route_base, Cow::Borrowed("/")) {
586 Cow::Owned(base) => Some(base),
587 Cow::Borrowed(_) => None,
588 };
589 self.rendered_components = std::mem::take(&mut context.rendered_components);
590 self.plugin = context.plugin.take();
591 self.route_children = std::mem::take(&mut context.route_children);
592 self.head_end_emitted = context.head_end_emitted;
593 self.body_start_emitted = context.body_start_emitted;
594 self.component_asset_styles_emitted = context.component_asset_styles_emitted;
595 self.body_end_emitted = context.body_end_emitted;
596 self.route_chain_index = context.route_chain_index;
597 self.route_chain = std::mem::take(&mut context.route_chain);
598 self.route_document_style_targets =
599 std::mem::take(&mut context.route_document_style_targets);
600 self.reachable_components = std::mem::take(&mut context.reachable_components);
601 self.json_scratch = std::mem::take(&mut context.json_scratch);
602 self.scope_pool = std::mem::take(&mut context.scope_pool);
603 self.shadow_style_roots = std::mem::take(&mut context.shadow_style_roots);
604 self.document_style_resources = std::mem::take(&mut context.document_style_resources);
605 self.streaming = Some(streaming.into_progress());
606 result
607 }
608
609 fn require_usable(&self, operation: &str) -> Result<()> {
610 if self.failed {
611 return Err(boundary_order_error(
612 operation,
613 "the session is poisoned by a previous render or transport failure; start a new response",
614 ));
615 }
616 Ok(())
617 }
618
619 fn require_started(&self, operation: &str) -> Result<()> {
620 if !self.started {
621 return Err(boundary_order_error(
622 operation,
623 "start must be called before this operation",
624 ));
625 }
626 Ok(())
627 }
628
629 fn require_resumable(&self) -> Result<()> {
635 self.require_usable("resume")?;
636 self.require_started("resume")?;
637 if self.done {
638 return Err(boundary_order_error(
639 "resume",
640 "the streaming response has already completed",
641 ));
642 }
643 if self.awaiting_advance {
644 return Err(boundary_order_error(
645 "resume",
646 "the previously committed boundary has not been advanced past; call advance \
647 before resuming the next occurrence",
648 ));
649 }
650 Ok(())
651 }
652
653 fn require_advanceable(&self) -> Result<()> {
655 self.require_usable("advance")?;
656 self.require_started("advance")?;
657 if self.done {
658 return Err(boundary_order_error(
659 "advance",
660 "the streaming response has already completed",
661 ));
662 }
663 if !self.awaiting_advance {
664 return Err(boundary_order_error(
665 "advance",
666 "there is no committed boundary to advance past; resume the pending occurrence \
667 first",
668 ));
669 }
670 Ok(())
671 }
672}
673
674#[cold]
675#[inline(never)]
676fn missing_progress_error() -> HandlerError {
677 HandlerError::Invariant("streaming progress is unavailable".to_string())
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683 use crate::{FlushWriter, ResponseWriter};
684 use webui_parser::{ComponentRegistration, HtmlParser};
685 use webui_protocol::{ComponentData, InitialStateStrategy, StateProjectionMode, WebUIProtocol};
686 use webui_test_utils::test_json;
687
688 const ISLAND_TAG: &str = "state-island";
689
690 struct TestSink {
691 output: String,
692 }
693
694 impl ResponseWriter for TestSink {
695 fn write(&mut self, content: &str) -> Result<()> {
696 self.output.push_str(content);
697 Ok(())
698 }
699
700 fn end(&mut self) -> Result<()> {
701 Ok(())
702 }
703 }
704
705 impl FlushWriter for TestSink {
706 fn flush(&mut self) -> Result<()> {
707 Ok(())
708 }
709 }
710
711 #[derive(Clone, Default)]
714 struct SharedSink(std::rc::Rc<std::cell::RefCell<String>>);
715
716 impl ResponseWriter for SharedSink {
717 fn write(&mut self, content: &str) -> Result<()> {
718 self.0.borrow_mut().push_str(content);
719 Ok(())
720 }
721
722 fn end(&mut self) -> Result<()> {
723 Ok(())
724 }
725 }
726
727 impl FlushWriter for SharedSink {
728 fn flush(&mut self) -> Result<()> {
729 Ok(())
730 }
731 }
732
733 fn boundary_protocol(boundaries: usize, hydration_mode: StateProjectionMode) -> Protocol {
736 let mut html = String::from("<!doctype html><html><head></head><body>");
737 for sequence in 0..boundaries {
738 html.push_str("<boundary name=\"b");
739 html.push_str(&sequence.to_string());
740 html.push_str("\"><article><");
741 html.push_str(ISLAND_TAG);
742 html.push_str("></");
743 html.push_str(ISLAND_TAG);
744 html.push_str("></article></boundary>");
745 }
746 html.push_str("</body></html>");
747
748 let mut parser = HtmlParser::new();
749 match parser
750 .component_registry_mut()
751 .register_component(ComponentRegistration::new(
752 ISLAND_TAG,
753 "<button>{{title}}</button>",
754 None,
755 true,
756 )) {
757 Ok(()) => {}
758 Err(error) => panic!("registering the island failed: {error}"),
759 }
760 if let Err(error) = parser.parse("index.html", &html) {
761 panic!("parsing the streaming entry failed: {error}");
762 }
763 let mut document = WebUIProtocol::new(parser.into_fragment_records());
764 document.initial_state_strategy = InitialStateStrategy::Components as i32;
765 document.components.insert(
766 ISLAND_TAG.to_string(),
767 ComponentData {
768 template_json: r#"{"h":"<button></button>","th":1}"#.to_string(),
769 uses_shadow_dom: true,
770 hydration_mode: hydration_mode as i32,
771 hydration_keys: if matches!(hydration_mode, StateProjectionMode::Keys) {
772 vec!["count".to_string(), "title".to_string()]
773 } else {
774 Vec::new()
775 },
776 ..Default::default()
777 },
778 );
779 document.populate_style_closures(&["index.html"]);
780 Protocol::new(document)
781 }
782
783 fn large_state(rows: usize) -> Value {
786 let mut items = Vec::with_capacity(rows);
787 for row in 0..rows {
788 items.push(test_json!({
789 "id": row,
790 "label": format!("row-{row}"),
791 "tags": ["alpha", "beta", "gamma"],
792 }));
793 }
794 test_json!({
795 "count": 42,
796 "title": "large state",
797 "rows": items,
798 })
799 }
800
801 fn rows_address(state: &Value) -> Option<usize> {
804 state
805 .get("rows")
806 .and_then(Value::as_array)
807 .map(|rows| rows.as_ptr().addr())
808 }
809
810 fn options<'a>() -> RenderOptions<'a> {
811 RenderOptions::new("index.html", "/")
812 }
813
814 #[test]
815 fn render_streaming_projects_full_state_once_per_response() -> Result<()> {
816 let protocol = boundary_protocol(8, StateProjectionMode::All);
821 let handler = WebUIHandler::new();
822 let state = large_state(256);
823 let render_options = options();
824 let mut sink = TestSink {
825 output: String::new(),
826 };
827 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
828
829 let mut status = response.start(&state)?;
830 let snapshot = rows_address(&response.core.frozen_state);
831 assert!(
832 snapshot.is_some(),
833 "a full-state protocol must retain the caller's payload"
834 );
835 assert_ne!(
836 snapshot,
837 rows_address(&state),
838 "the response owns its snapshot rather than borrowing the caller's tree"
839 );
840
841 let mut committed = 0usize;
842 while !status.done {
843 status = match status.boundary.as_ref() {
844 Some(boundary) => {
845 let next =
846 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
847 committed += 1;
848 assert!(
849 next.boundary.is_none() && !next.done,
850 "a commit step stops at its checkpoint and waits for advance"
851 );
852 assert_eq!(
853 rows_address(&response.core.frozen_state),
854 snapshot,
855 "committing occurrence {committed} must not re-copy the retained snapshot"
856 );
857 next
858 }
859 None => response.advance()?,
860 };
861 }
862 assert_eq!(committed, 8, "every authored boundary must commit");
863
864 let mut helper_sink = TestSink {
867 output: String::new(),
868 };
869 handler.render_streaming(&protocol, &state, &render_options, &mut helper_sink)?;
870 assert_eq!(
871 helper_sink.output, sink.output,
872 "render_streaming must resume against the retained snapshot"
873 );
874 Ok(())
875 }
876
877 #[test]
878 fn fused_helper_matches_split_step_bytes_and_flushes() -> Result<()> {
879 #[derive(Default)]
884 struct FlushSink {
885 output: String,
886 flushes: Vec<usize>,
887 }
888
889 impl ResponseWriter for FlushSink {
890 fn write(&mut self, content: &str) -> Result<()> {
891 self.output.push_str(content);
892 Ok(())
893 }
894
895 fn end(&mut self) -> Result<()> {
896 Ok(())
897 }
898 }
899
900 impl FlushWriter for FlushSink {
901 fn flush(&mut self) -> Result<()> {
902 self.flushes.push(self.output.len());
903 Ok(())
904 }
905 }
906
907 let protocol = boundary_protocol(4, StateProjectionMode::Keys);
908 let handler = WebUIHandler::new();
909 let state = test_json!({ "count": 3, "title": "fused" });
910 let render_options = options();
911
912 let mut split = FlushSink::default();
913 {
914 let mut response = handler.stream_response(&protocol, &render_options, &mut split)?;
915 let mut status = response.start(&state)?;
916 while !status.done {
917 status = match status.boundary.as_ref() {
918 Some(boundary) => {
919 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?
920 }
921 None => response.advance()?,
922 };
923 }
924 }
925
926 let mut fused = FlushSink::default();
927 handler.render_streaming(&protocol, &state, &render_options, &mut fused)?;
928
929 assert_eq!(fused.output, split.output, "bytes must match");
930 assert_eq!(fused.flushes, split.flushes, "flush positions must match");
931 Ok(())
932 }
933
934 #[test]
935 fn resume_overlays_new_state_and_reuses_unchanged_subtrees() -> Result<()> {
936 let protocol = boundary_protocol(2, StateProjectionMode::All);
940 let handler = WebUIHandler::new();
941 let state = large_state(64);
942 let render_options = options();
943 let mut sink = TestSink {
944 output: String::new(),
945 };
946 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
947
948 let status = response.start(&state)?;
949 let snapshot = rows_address(&response.core.frozen_state);
950 let Some(boundary) = status.boundary.as_ref() else {
951 panic!("the first occurrence must suspend");
952 };
953
954 let mut next = state.clone();
955 if let Some(object) = next.as_object_mut() {
956 object.insert("title".to_string(), Value::String("second".to_string()));
957 object.remove("count");
958 }
959 let status = response.resume(boundary.instance_id, &next, BoundaryMode::Final)?;
960
961 assert_eq!(
962 response.core.frozen_state.get("title"),
963 Some(&Value::String("second".to_string())),
964 "a changed key must land in the snapshot"
965 );
966 assert_eq!(
967 response.core.frozen_state.get("count"),
968 Some(&test_json!(42)),
969 "an omitted key keeps the value the snapshot already holds"
970 );
971 assert_eq!(
972 rows_address(&response.core.frozen_state),
973 snapshot,
974 "an unchanged subtree must not be copied again"
975 );
976 assert!(
977 status.boundary.is_none() && !status.done,
978 "the commit step stops at its checkpoint"
979 );
980 assert!(
981 response.advance()?.boundary.is_some(),
982 "the second occurrence follows the parent bytes"
983 );
984 Ok(())
985 }
986
987 #[test]
988 fn semantic_steps_reuse_projection_scratch() -> Result<()> {
989 let protocol = boundary_protocol(6, StateProjectionMode::Keys);
993 let handler = WebUIHandler::new();
994 let state = test_json!({ "count": 1, "title": "scratch", "unused": "x" });
995 let render_options = options();
996 let mut sink = TestSink {
997 output: String::new(),
998 };
999 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1000
1001 let mut status = response.start(&state)?;
1002 let mut retained: Option<(usize, usize)> = None;
1003 while !status.done {
1004 let Some(boundary) = status.boundary.as_ref() else {
1005 status = response.advance()?;
1006 continue;
1007 };
1008 status = response.resume(boundary.instance_id, &state, BoundaryMode::Updatable)?;
1009 let Some(progress) = response.core.streaming.as_ref() else {
1010 panic!("a suspended response must retain its progress");
1011 };
1012 let observed = (
1013 progress.state_key_ids.capacity(),
1014 progress.state_key_ids.as_ptr().addr(),
1015 );
1016 assert!(
1017 observed.0 > 0,
1018 "the projection scratch must survive the record that filled it"
1019 );
1020 match retained {
1021 None => retained = Some(observed),
1022 Some(previous) => assert_eq!(
1023 observed, previous,
1024 "a later step must reuse the projection buffer, not allocate a new one"
1025 ),
1026 }
1027 }
1028 Ok(())
1029 }
1030
1031 #[test]
1032 fn updates_reuse_their_committed_projection_buffer() -> Result<()> {
1033 let protocol = boundary_protocol(2, StateProjectionMode::Keys);
1037 let handler = WebUIHandler::new();
1038 let state = test_json!({ "count": 1, "title": "updates" });
1039 let render_options = options();
1040 let mut sink = TestSink {
1041 output: String::new(),
1042 };
1043 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1044
1045 let status = response.start(&state)?;
1046 let Some(boundary) = status.boundary.as_ref() else {
1047 panic!("the first occurrence must suspend");
1048 };
1049 let instance_id = boundary.instance_id;
1050 response.resume(instance_id, &state, BoundaryMode::Updatable)?;
1051
1052 let mut retained: Option<(usize, usize)> = None;
1053 for _ in 0..4 {
1054 response.update(instance_id, &state)?;
1055 let Some(progress) = response.core.streaming.as_ref() else {
1056 panic!("a live response must retain its progress");
1057 };
1058 let Some(Some(plan)) = progress.update_plans.first() else {
1059 panic!("an updatable occurrence must retain its projection plan");
1060 };
1061 let observed = (plan.key_ids.capacity(), plan.key_ids.as_ptr().addr());
1062 assert!(!plan.requires_full_state, "keyed islands project keys");
1063 assert!(observed.0 > 0, "the plan must retain its key buffer");
1064 match retained {
1065 None => retained = Some(observed),
1066 Some(previous) => assert_eq!(
1067 observed, previous,
1068 "every update must reuse the committed projection buffer"
1069 ),
1070 }
1071 }
1072 assert_eq!(
1073 sink.output.matches(",2,0,{").count(),
1074 4,
1075 "each update emits exactly one typed state-update record"
1076 );
1077 Ok(())
1078 }
1079
1080 fn commit_occurrences<W: FlushWriter + ?Sized>(
1083 response: &mut StreamingResponse<'_, W>,
1084 status: StreamStatus,
1085 state: &Value,
1086 count: usize,
1087 mode: BoundaryMode,
1088 ) -> Result<StreamStatus> {
1089 let mut status = status;
1090 for committed in 0..count {
1091 let Some(boundary) = status.boundary.as_ref() else {
1092 panic!("occurrence {committed} must suspend before it can commit");
1093 };
1094 let instance_id = boundary.instance_id;
1095 response.resume(instance_id, state, mode)?;
1096 status = response.advance()?;
1097 }
1098 Ok(status)
1099 }
1100
1101 #[test]
1102 fn updatable_commits_stop_at_the_browser_retention_cap() -> Result<()> {
1103 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 1, StateProjectionMode::Keys);
1109 let handler = WebUIHandler::new();
1110 let state = test_json!({ "count": 1, "title": "retained" });
1111 let render_options = options();
1112 let mut sink = SharedSink::default();
1113 let written = SharedSink::clone(&sink).0;
1114 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1115
1116 let status = response.start(&state)?;
1117 let status = commit_occurrences(
1118 &mut response,
1119 status,
1120 &state,
1121 MAX_UPDATABLE_OCCURRENCES,
1122 BoundaryMode::Updatable,
1123 )?;
1124
1125 let Some(boundary) = status.boundary.as_ref() else {
1126 panic!("the occurrence past the cap must suspend like any other");
1127 };
1128 let instance_id = boundary.instance_id;
1129 let bytes_before = written.borrow().len();
1130 let refused = test_json!({ "count": 2, "title": "refused" });
1131 let rejected = response.resume(instance_id, &refused, BoundaryMode::Updatable);
1132 match rejected {
1133 Err(HandlerError::StreamingBoundary(error)) => {
1134 assert_eq!(error.signal, "resume");
1135 assert!(
1136 error.reason.contains("BoundaryMode::Final"),
1137 "the refusal must name the recovery: {}",
1138 error.reason
1139 );
1140 }
1141 _ => panic!("committing past the cap must fail with a typed boundary error"),
1142 }
1143 assert_eq!(
1144 written.borrow().len(),
1145 bytes_before,
1146 "a refused commit must not write a byte"
1147 );
1148 assert_eq!(
1149 response.core.frozen_state.get("title"),
1150 Some(&Value::String("retained".to_string())),
1151 "a refused commit must not merge its state into the snapshot"
1152 );
1153
1154 let status = response.resume(instance_id, &state, BoundaryMode::Final)?;
1156 assert!(
1157 status.boundary.is_none() && !status.done,
1158 "the retried commit stops at its own checkpoint"
1159 );
1160 assert!(
1161 written.borrow().len() > bytes_before,
1162 "the retry writes its checkpoint"
1163 );
1164 assert!(
1165 response.advance()?.done,
1166 "the response still reaches its terminal"
1167 );
1168 Ok(())
1169 }
1170
1171 #[test]
1172 fn final_commits_do_not_consume_the_updatable_cap() -> Result<()> {
1173 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 2, StateProjectionMode::Keys);
1177 let handler = WebUIHandler::new();
1178 let state = test_json!({ "count": 1, "title": "mixed" });
1179 let render_options = options();
1180 let mut sink = TestSink {
1181 output: String::new(),
1182 };
1183 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1184
1185 let status = response.start(&state)?;
1186 let status = commit_occurrences(&mut response, status, &state, 2, BoundaryMode::Final)?;
1187 let status = commit_occurrences(
1188 &mut response,
1189 status,
1190 &state,
1191 MAX_UPDATABLE_OCCURRENCES,
1192 BoundaryMode::Updatable,
1193 )?;
1194 assert!(
1195 status.done && response.is_done(),
1196 "final commits must leave the whole updatable budget available"
1197 );
1198 Ok(())
1199 }
1200
1201 #[test]
1202 fn owned_sessions_enforce_the_same_updatable_cap() -> Result<()> {
1203 let protocol = Arc::new(boundary_protocol(
1206 MAX_UPDATABLE_OCCURRENCES + 1,
1207 StateProjectionMode::Keys,
1208 ));
1209 let mut session = crate::streaming::StreamingSession::new(
1210 Arc::new(WebUIHandler::new()),
1211 protocol,
1212 crate::streaming::SessionOptions::new("index.html", "/"),
1213 )?;
1214 let state = test_json!({ "count": 1, "title": "owned" });
1215
1216 let mut step = session.start(&state)?;
1217 for committed in 0..MAX_UPDATABLE_OCCURRENCES {
1218 let Some(boundary) = step.boundary.as_ref() else {
1219 panic!("occurrence {committed} must suspend before it can commit");
1220 };
1221 let instance_id = boundary.instance_id;
1222 session.resume(instance_id, &state, BoundaryMode::Updatable)?;
1223 step = session.advance()?;
1224 }
1225
1226 let Some(boundary) = step.boundary.as_ref() else {
1227 panic!("the occurrence past the cap must suspend like any other");
1228 };
1229 let instance_id = boundary.instance_id;
1230 assert!(
1231 session
1232 .resume(instance_id, &state, BoundaryMode::Updatable)
1233 .is_err(),
1234 "an owned session refuses the occurrence past the cap"
1235 );
1236 let step = session.resume(instance_id, &state, BoundaryMode::Final)?;
1237 assert!(
1238 !step.bytes.is_empty(),
1239 "the retried commit still delivers its checkpoint bytes"
1240 );
1241 assert!(
1242 session.advance()?.done,
1243 "the owned response still reaches its terminal"
1244 );
1245 Ok(())
1246 }
1247}