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_chunk_index: protocol.protocol().style_chunk_index(),
561 css_strategy: protocol.css_strategy(),
562 head_inject: options.head_inject.filter(|html| !html.is_empty()),
563 body_inject: options.body_inject.filter(|html| !html.is_empty()),
564 state_inject: crate::StateInject::resolve(state),
565 head_end_emitted: self.head_end_emitted,
566 body_start_emitted: self.body_start_emitted,
567 component_asset_styles_emitted: self.component_asset_styles_emitted,
568 body_end_emitted: self.body_end_emitted,
569 route_index: protocol.route_index(),
570 route_chain_index: self.route_chain_index,
571 route_chain: std::mem::take(&mut self.route_chain),
572 route_document_style_targets: std::mem::take(&mut self.route_document_style_targets),
573 reachable_components: std::mem::take(&mut self.reachable_components),
574 streaming: Some(&mut streaming),
575 json_scratch: std::mem::take(&mut self.json_scratch),
576 scope_pool: std::mem::take(&mut self.scope_pool),
577 document_style_resources: std::mem::take(&mut self.document_style_resources),
578 shadow_style_roots: std::mem::take(&mut self.shadow_style_roots),
579 borrowed_scope_pool: Vec::new(),
580 };
581 let result = operation(&mut self.vm, &mut context);
582 self.local_vars = std::mem::take(&mut context.local_vars);
583 self.component_attrs = std::mem::take(&mut context.component_attrs);
584 self.route_base = match std::mem::replace(&mut context.route_base, Cow::Borrowed("/")) {
585 Cow::Owned(base) => Some(base),
586 Cow::Borrowed(_) => None,
587 };
588 self.rendered_components = std::mem::take(&mut context.rendered_components);
589 self.plugin = context.plugin.take();
590 self.route_children = std::mem::take(&mut context.route_children);
591 self.head_end_emitted = context.head_end_emitted;
592 self.body_start_emitted = context.body_start_emitted;
593 self.component_asset_styles_emitted = context.component_asset_styles_emitted;
594 self.body_end_emitted = context.body_end_emitted;
595 self.route_chain_index = context.route_chain_index;
596 self.route_chain = std::mem::take(&mut context.route_chain);
597 self.route_document_style_targets =
598 std::mem::take(&mut context.route_document_style_targets);
599 self.reachable_components = std::mem::take(&mut context.reachable_components);
600 self.json_scratch = std::mem::take(&mut context.json_scratch);
601 self.scope_pool = std::mem::take(&mut context.scope_pool);
602 self.shadow_style_roots = std::mem::take(&mut context.shadow_style_roots);
603 self.document_style_resources = std::mem::take(&mut context.document_style_resources);
604 self.streaming = Some(streaming.into_progress());
605 result
606 }
607
608 fn require_usable(&self, operation: &str) -> Result<()> {
609 if self.failed {
610 return Err(boundary_order_error(
611 operation,
612 "the session is poisoned by a previous render or transport failure; start a new response",
613 ));
614 }
615 Ok(())
616 }
617
618 fn require_started(&self, operation: &str) -> Result<()> {
619 if !self.started {
620 return Err(boundary_order_error(
621 operation,
622 "start must be called before this operation",
623 ));
624 }
625 Ok(())
626 }
627
628 fn require_resumable(&self) -> Result<()> {
634 self.require_usable("resume")?;
635 self.require_started("resume")?;
636 if self.done {
637 return Err(boundary_order_error(
638 "resume",
639 "the streaming response has already completed",
640 ));
641 }
642 if self.awaiting_advance {
643 return Err(boundary_order_error(
644 "resume",
645 "the previously committed boundary has not been advanced past; call advance \
646 before resuming the next occurrence",
647 ));
648 }
649 Ok(())
650 }
651
652 fn require_advanceable(&self) -> Result<()> {
654 self.require_usable("advance")?;
655 self.require_started("advance")?;
656 if self.done {
657 return Err(boundary_order_error(
658 "advance",
659 "the streaming response has already completed",
660 ));
661 }
662 if !self.awaiting_advance {
663 return Err(boundary_order_error(
664 "advance",
665 "there is no committed boundary to advance past; resume the pending occurrence \
666 first",
667 ));
668 }
669 Ok(())
670 }
671}
672
673#[cold]
674#[inline(never)]
675fn missing_progress_error() -> HandlerError {
676 HandlerError::Invariant("streaming progress is unavailable".to_string())
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::{FlushWriter, ResponseWriter};
683 use webui_parser::{ComponentRegistration, HtmlParser};
684 use webui_protocol::{ComponentData, InitialStateStrategy, StateProjectionMode, WebUIProtocol};
685 use webui_test_utils::test_json;
686
687 const ISLAND_TAG: &str = "state-island";
688
689 struct TestSink {
690 output: String,
691 }
692
693 impl ResponseWriter for TestSink {
694 fn write(&mut self, content: &str) -> Result<()> {
695 self.output.push_str(content);
696 Ok(())
697 }
698
699 fn end(&mut self) -> Result<()> {
700 Ok(())
701 }
702 }
703
704 impl FlushWriter for TestSink {
705 fn flush(&mut self) -> Result<()> {
706 Ok(())
707 }
708 }
709
710 #[derive(Clone, Default)]
713 struct SharedSink(std::rc::Rc<std::cell::RefCell<String>>);
714
715 impl ResponseWriter for SharedSink {
716 fn write(&mut self, content: &str) -> Result<()> {
717 self.0.borrow_mut().push_str(content);
718 Ok(())
719 }
720
721 fn end(&mut self) -> Result<()> {
722 Ok(())
723 }
724 }
725
726 impl FlushWriter for SharedSink {
727 fn flush(&mut self) -> Result<()> {
728 Ok(())
729 }
730 }
731
732 fn boundary_protocol(boundaries: usize, hydration_mode: StateProjectionMode) -> Protocol {
735 let mut html = String::from("<!doctype html><html><head></head><body>");
736 for sequence in 0..boundaries {
737 html.push_str("<boundary name=\"b");
738 html.push_str(&sequence.to_string());
739 html.push_str("\"><article><");
740 html.push_str(ISLAND_TAG);
741 html.push_str("></");
742 html.push_str(ISLAND_TAG);
743 html.push_str("></article></boundary>");
744 }
745 html.push_str("</body></html>");
746
747 let mut parser = HtmlParser::new();
748 match parser
749 .component_registry_mut()
750 .register_component(ComponentRegistration::new(
751 ISLAND_TAG,
752 "<button>{{title}}</button>",
753 None,
754 true,
755 )) {
756 Ok(()) => {}
757 Err(error) => panic!("registering the island failed: {error}"),
758 }
759 if let Err(error) = parser.parse("index.html", &html) {
760 panic!("parsing the streaming entry failed: {error}");
761 }
762 let mut document = WebUIProtocol::new(parser.into_fragment_records());
763 document.initial_state_strategy = InitialStateStrategy::Components as i32;
764 document.components.insert(
765 ISLAND_TAG.to_string(),
766 ComponentData {
767 template_json: r#"{"h":"<button></button>","th":1}"#.to_string(),
768 uses_shadow_dom: true,
769 hydration_mode: hydration_mode as i32,
770 hydration_keys: if matches!(hydration_mode, StateProjectionMode::Keys) {
771 vec!["count".to_string(), "title".to_string()]
772 } else {
773 Vec::new()
774 },
775 ..Default::default()
776 },
777 );
778 document.populate_style_closures(&["index.html"]);
779 Protocol::new(document)
780 }
781
782 fn large_state(rows: usize) -> Value {
785 let mut items = Vec::with_capacity(rows);
786 for row in 0..rows {
787 items.push(test_json!({
788 "id": row,
789 "label": format!("row-{row}"),
790 "tags": ["alpha", "beta", "gamma"],
791 }));
792 }
793 test_json!({
794 "count": 42,
795 "title": "large state",
796 "rows": items,
797 })
798 }
799
800 fn rows_address(state: &Value) -> Option<usize> {
803 state
804 .get("rows")
805 .and_then(Value::as_array)
806 .map(|rows| rows.as_ptr().addr())
807 }
808
809 fn options<'a>() -> RenderOptions<'a> {
810 RenderOptions::new("index.html", "/")
811 }
812
813 #[test]
814 fn render_streaming_projects_full_state_once_per_response() -> Result<()> {
815 let protocol = boundary_protocol(8, StateProjectionMode::All);
820 let handler = WebUIHandler::new();
821 let state = large_state(256);
822 let render_options = options();
823 let mut sink = TestSink {
824 output: String::new(),
825 };
826 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
827
828 let mut status = response.start(&state)?;
829 let snapshot = rows_address(&response.core.frozen_state);
830 assert!(
831 snapshot.is_some(),
832 "a full-state protocol must retain the caller's payload"
833 );
834 assert_ne!(
835 snapshot,
836 rows_address(&state),
837 "the response owns its snapshot rather than borrowing the caller's tree"
838 );
839
840 let mut committed = 0usize;
841 while !status.done {
842 status = match status.boundary.as_ref() {
843 Some(boundary) => {
844 let next =
845 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
846 committed += 1;
847 assert!(
848 next.boundary.is_none() && !next.done,
849 "a commit step stops at its checkpoint and waits for advance"
850 );
851 assert_eq!(
852 rows_address(&response.core.frozen_state),
853 snapshot,
854 "committing occurrence {committed} must not re-copy the retained snapshot"
855 );
856 next
857 }
858 None => response.advance()?,
859 };
860 }
861 assert_eq!(committed, 8, "every authored boundary must commit");
862
863 let mut helper_sink = TestSink {
866 output: String::new(),
867 };
868 handler.render_streaming(&protocol, &state, &render_options, &mut helper_sink)?;
869 assert_eq!(
870 helper_sink.output, sink.output,
871 "render_streaming must resume against the retained snapshot"
872 );
873 Ok(())
874 }
875
876 #[test]
877 fn fused_helper_matches_split_step_bytes_and_flushes() -> Result<()> {
878 #[derive(Default)]
883 struct FlushSink {
884 output: String,
885 flushes: Vec<usize>,
886 }
887
888 impl ResponseWriter for FlushSink {
889 fn write(&mut self, content: &str) -> Result<()> {
890 self.output.push_str(content);
891 Ok(())
892 }
893
894 fn end(&mut self) -> Result<()> {
895 Ok(())
896 }
897 }
898
899 impl FlushWriter for FlushSink {
900 fn flush(&mut self) -> Result<()> {
901 self.flushes.push(self.output.len());
902 Ok(())
903 }
904 }
905
906 let protocol = boundary_protocol(4, StateProjectionMode::Keys);
907 let handler = WebUIHandler::new();
908 let state = test_json!({ "count": 3, "title": "fused" });
909 let render_options = options();
910
911 let mut split = FlushSink::default();
912 {
913 let mut response = handler.stream_response(&protocol, &render_options, &mut split)?;
914 let mut status = response.start(&state)?;
915 while !status.done {
916 status = match status.boundary.as_ref() {
917 Some(boundary) => {
918 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?
919 }
920 None => response.advance()?,
921 };
922 }
923 }
924
925 let mut fused = FlushSink::default();
926 handler.render_streaming(&protocol, &state, &render_options, &mut fused)?;
927
928 assert_eq!(fused.output, split.output, "bytes must match");
929 assert_eq!(fused.flushes, split.flushes, "flush positions must match");
930 Ok(())
931 }
932
933 #[test]
934 fn resume_overlays_new_state_and_reuses_unchanged_subtrees() -> Result<()> {
935 let protocol = boundary_protocol(2, StateProjectionMode::All);
939 let handler = WebUIHandler::new();
940 let state = large_state(64);
941 let render_options = options();
942 let mut sink = TestSink {
943 output: String::new(),
944 };
945 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
946
947 let status = response.start(&state)?;
948 let snapshot = rows_address(&response.core.frozen_state);
949 let Some(boundary) = status.boundary.as_ref() else {
950 panic!("the first occurrence must suspend");
951 };
952
953 let mut next = state.clone();
954 if let Some(object) = next.as_object_mut() {
955 object.insert("title".to_string(), Value::String("second".to_string()));
956 object.remove("count");
957 }
958 let status = response.resume(boundary.instance_id, &next, BoundaryMode::Final)?;
959
960 assert_eq!(
961 response.core.frozen_state.get("title"),
962 Some(&Value::String("second".to_string())),
963 "a changed key must land in the snapshot"
964 );
965 assert_eq!(
966 response.core.frozen_state.get("count"),
967 Some(&test_json!(42)),
968 "an omitted key keeps the value the snapshot already holds"
969 );
970 assert_eq!(
971 rows_address(&response.core.frozen_state),
972 snapshot,
973 "an unchanged subtree must not be copied again"
974 );
975 assert!(
976 status.boundary.is_none() && !status.done,
977 "the commit step stops at its checkpoint"
978 );
979 assert!(
980 response.advance()?.boundary.is_some(),
981 "the second occurrence follows the parent bytes"
982 );
983 Ok(())
984 }
985
986 #[test]
987 fn semantic_steps_reuse_projection_scratch() -> Result<()> {
988 let protocol = boundary_protocol(6, StateProjectionMode::Keys);
992 let handler = WebUIHandler::new();
993 let state = test_json!({ "count": 1, "title": "scratch", "unused": "x" });
994 let render_options = options();
995 let mut sink = TestSink {
996 output: String::new(),
997 };
998 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
999
1000 let mut status = response.start(&state)?;
1001 let mut retained: Option<(usize, usize)> = None;
1002 while !status.done {
1003 let Some(boundary) = status.boundary.as_ref() else {
1004 status = response.advance()?;
1005 continue;
1006 };
1007 status = response.resume(boundary.instance_id, &state, BoundaryMode::Updatable)?;
1008 let Some(progress) = response.core.streaming.as_ref() else {
1009 panic!("a suspended response must retain its progress");
1010 };
1011 let observed = (
1012 progress.state_key_ids.capacity(),
1013 progress.state_key_ids.as_ptr().addr(),
1014 );
1015 assert!(
1016 observed.0 > 0,
1017 "the projection scratch must survive the record that filled it"
1018 );
1019 match retained {
1020 None => retained = Some(observed),
1021 Some(previous) => assert_eq!(
1022 observed, previous,
1023 "a later step must reuse the projection buffer, not allocate a new one"
1024 ),
1025 }
1026 }
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn updates_reuse_their_committed_projection_buffer() -> Result<()> {
1032 let protocol = boundary_protocol(2, StateProjectionMode::Keys);
1036 let handler = WebUIHandler::new();
1037 let state = test_json!({ "count": 1, "title": "updates" });
1038 let render_options = options();
1039 let mut sink = TestSink {
1040 output: String::new(),
1041 };
1042 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1043
1044 let status = response.start(&state)?;
1045 let Some(boundary) = status.boundary.as_ref() else {
1046 panic!("the first occurrence must suspend");
1047 };
1048 let instance_id = boundary.instance_id;
1049 response.resume(instance_id, &state, BoundaryMode::Updatable)?;
1050
1051 let mut retained: Option<(usize, usize)> = None;
1052 for _ in 0..4 {
1053 response.update(instance_id, &state)?;
1054 let Some(progress) = response.core.streaming.as_ref() else {
1055 panic!("a live response must retain its progress");
1056 };
1057 let Some(Some(plan)) = progress.update_plans.first() else {
1058 panic!("an updatable occurrence must retain its projection plan");
1059 };
1060 let observed = (plan.key_ids.capacity(), plan.key_ids.as_ptr().addr());
1061 assert!(!plan.requires_full_state, "keyed islands project keys");
1062 assert!(observed.0 > 0, "the plan must retain its key buffer");
1063 match retained {
1064 None => retained = Some(observed),
1065 Some(previous) => assert_eq!(
1066 observed, previous,
1067 "every update must reuse the committed projection buffer"
1068 ),
1069 }
1070 }
1071 assert_eq!(
1072 sink.output.matches(",2,0,{").count(),
1073 4,
1074 "each update emits exactly one typed state-update record"
1075 );
1076 Ok(())
1077 }
1078
1079 fn commit_occurrences<W: FlushWriter + ?Sized>(
1082 response: &mut StreamingResponse<'_, W>,
1083 status: StreamStatus,
1084 state: &Value,
1085 count: usize,
1086 mode: BoundaryMode,
1087 ) -> Result<StreamStatus> {
1088 let mut status = status;
1089 for committed in 0..count {
1090 let Some(boundary) = status.boundary.as_ref() else {
1091 panic!("occurrence {committed} must suspend before it can commit");
1092 };
1093 let instance_id = boundary.instance_id;
1094 response.resume(instance_id, state, mode)?;
1095 status = response.advance()?;
1096 }
1097 Ok(status)
1098 }
1099
1100 #[test]
1101 fn updatable_commits_stop_at_the_browser_retention_cap() -> Result<()> {
1102 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 1, StateProjectionMode::Keys);
1108 let handler = WebUIHandler::new();
1109 let state = test_json!({ "count": 1, "title": "retained" });
1110 let render_options = options();
1111 let mut sink = SharedSink::default();
1112 let written = SharedSink::clone(&sink).0;
1113 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1114
1115 let status = response.start(&state)?;
1116 let status = commit_occurrences(
1117 &mut response,
1118 status,
1119 &state,
1120 MAX_UPDATABLE_OCCURRENCES,
1121 BoundaryMode::Updatable,
1122 )?;
1123
1124 let Some(boundary) = status.boundary.as_ref() else {
1125 panic!("the occurrence past the cap must suspend like any other");
1126 };
1127 let instance_id = boundary.instance_id;
1128 let bytes_before = written.borrow().len();
1129 let refused = test_json!({ "count": 2, "title": "refused" });
1130 let rejected = response.resume(instance_id, &refused, BoundaryMode::Updatable);
1131 match rejected {
1132 Err(HandlerError::StreamingBoundary(error)) => {
1133 assert_eq!(error.signal, "resume");
1134 assert!(
1135 error.reason.contains("BoundaryMode::Final"),
1136 "the refusal must name the recovery: {}",
1137 error.reason
1138 );
1139 }
1140 _ => panic!("committing past the cap must fail with a typed boundary error"),
1141 }
1142 assert_eq!(
1143 written.borrow().len(),
1144 bytes_before,
1145 "a refused commit must not write a byte"
1146 );
1147 assert_eq!(
1148 response.core.frozen_state.get("title"),
1149 Some(&Value::String("retained".to_string())),
1150 "a refused commit must not merge its state into the snapshot"
1151 );
1152
1153 let status = response.resume(instance_id, &state, BoundaryMode::Final)?;
1155 assert!(
1156 status.boundary.is_none() && !status.done,
1157 "the retried commit stops at its own checkpoint"
1158 );
1159 assert!(
1160 written.borrow().len() > bytes_before,
1161 "the retry writes its checkpoint"
1162 );
1163 assert!(
1164 response.advance()?.done,
1165 "the response still reaches its terminal"
1166 );
1167 Ok(())
1168 }
1169
1170 #[test]
1171 fn final_commits_do_not_consume_the_updatable_cap() -> Result<()> {
1172 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 2, StateProjectionMode::Keys);
1176 let handler = WebUIHandler::new();
1177 let state = test_json!({ "count": 1, "title": "mixed" });
1178 let render_options = options();
1179 let mut sink = TestSink {
1180 output: String::new(),
1181 };
1182 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1183
1184 let status = response.start(&state)?;
1185 let status = commit_occurrences(&mut response, status, &state, 2, BoundaryMode::Final)?;
1186 let status = commit_occurrences(
1187 &mut response,
1188 status,
1189 &state,
1190 MAX_UPDATABLE_OCCURRENCES,
1191 BoundaryMode::Updatable,
1192 )?;
1193 assert!(
1194 status.done && response.is_done(),
1195 "final commits must leave the whole updatable budget available"
1196 );
1197 Ok(())
1198 }
1199
1200 #[test]
1201 fn owned_sessions_enforce_the_same_updatable_cap() -> Result<()> {
1202 let protocol = Arc::new(boundary_protocol(
1205 MAX_UPDATABLE_OCCURRENCES + 1,
1206 StateProjectionMode::Keys,
1207 ));
1208 let mut session = crate::streaming::StreamingSession::new(
1209 Arc::new(WebUIHandler::new()),
1210 protocol,
1211 crate::streaming::SessionOptions::new("index.html", "/"),
1212 )?;
1213 let state = test_json!({ "count": 1, "title": "owned" });
1214
1215 let mut step = session.start(&state)?;
1216 for committed in 0..MAX_UPDATABLE_OCCURRENCES {
1217 let Some(boundary) = step.boundary.as_ref() else {
1218 panic!("occurrence {committed} must suspend before it can commit");
1219 };
1220 let instance_id = boundary.instance_id;
1221 session.resume(instance_id, &state, BoundaryMode::Updatable)?;
1222 step = session.advance()?;
1223 }
1224
1225 let Some(boundary) = step.boundary.as_ref() else {
1226 panic!("the occurrence past the cap must suspend like any other");
1227 };
1228 let instance_id = boundary.instance_id;
1229 assert!(
1230 session
1231 .resume(instance_id, &state, BoundaryMode::Updatable)
1232 .is_err(),
1233 "an owned session refuses the occurrence past the cap"
1234 );
1235 let step = session.resume(instance_id, &state, BoundaryMode::Final)?;
1236 assert!(
1237 !step.bytes.is_empty(),
1238 "the retried commit still delivers its checkpoint bytes"
1239 );
1240 assert!(
1241 session.advance()?.done,
1242 "the owned response still reaches its terminal"
1243 );
1244 Ok(())
1245 }
1246}