1use std::borrow::Cow;
7use std::collections::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: crate::ScopeMap,
362 component_attrs: crate::ScopeMap,
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<crate::ScopeMap>,
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: crate::ScopeMap::default(),
405 component_attrs: crate::ScopeMap::default(),
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(crate::ScopeMap::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
986 .core
987 .scope_pool
988 .first()
989 .map(crate::ScopeMap::capacity),
990 Some(pooled_capacity),
991 "a suspension step must preserve the scope-map pool"
992 );
993 Ok(())
994 }
995
996 #[test]
997 fn render_streaming_projects_full_state_once_per_response() -> Result<()> {
998 let protocol = boundary_protocol(8, StateProjectionMode::All);
1003 let handler = WebUIHandler::new();
1004 let state = large_state(256);
1005 let render_options = options();
1006 let mut sink = TestSink {
1007 output: String::new(),
1008 };
1009 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1010
1011 let mut status = response.start(&state)?;
1012 let snapshot = rows_address(&response.core.frozen_state);
1013 assert!(
1014 snapshot.is_some(),
1015 "a full-state protocol must retain the caller's payload"
1016 );
1017 assert_ne!(
1018 snapshot,
1019 rows_address(&state),
1020 "the response owns its snapshot rather than borrowing the caller's tree"
1021 );
1022
1023 let mut committed = 0usize;
1024 while !status.done {
1025 status = match status.boundary.as_ref() {
1026 Some(boundary) => {
1027 let next =
1028 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
1029 committed += 1;
1030 assert!(
1031 next.boundary.is_none() && !next.done,
1032 "a commit step stops at its checkpoint and waits for advance"
1033 );
1034 assert_eq!(
1035 rows_address(&response.core.frozen_state),
1036 snapshot,
1037 "committing occurrence {committed} must not re-copy the retained snapshot"
1038 );
1039 next
1040 }
1041 None => response.advance()?,
1042 };
1043 }
1044 assert_eq!(committed, 8, "every authored boundary must commit");
1045
1046 let mut helper_sink = TestSink {
1049 output: String::new(),
1050 };
1051 handler.render_streaming(&protocol, &state, &render_options, &mut helper_sink)?;
1052 assert_eq!(
1053 helper_sink.output, sink.output,
1054 "render_streaming must resume against the retained snapshot"
1055 );
1056 Ok(())
1057 }
1058
1059 #[test]
1060 fn fused_helper_matches_split_step_bytes_and_flushes() -> Result<()> {
1061 #[derive(Default)]
1066 struct FlushSink {
1067 output: String,
1068 flushes: Vec<usize>,
1069 }
1070
1071 impl ResponseWriter for FlushSink {
1072 fn write(&mut self, content: &str) -> Result<()> {
1073 self.output.push_str(content);
1074 Ok(())
1075 }
1076
1077 fn end(&mut self) -> Result<()> {
1078 Ok(())
1079 }
1080 }
1081
1082 impl FlushWriter for FlushSink {
1083 fn flush(&mut self) -> Result<()> {
1084 self.flushes.push(self.output.len());
1085 Ok(())
1086 }
1087 }
1088
1089 let protocol = boundary_protocol(4, StateProjectionMode::Keys);
1090 let handler = WebUIHandler::new();
1091 let state = test_json!({ "count": 3, "title": "fused" });
1092 let render_options = options();
1093
1094 let mut split = FlushSink::default();
1095 {
1096 let mut response = handler.stream_response(&protocol, &render_options, &mut split)?;
1097 let mut status = response.start(&state)?;
1098 while !status.done {
1099 status = match status.boundary.as_ref() {
1100 Some(boundary) => {
1101 response.resume(boundary.instance_id, &state, BoundaryMode::Final)?
1102 }
1103 None => response.advance()?,
1104 };
1105 }
1106 }
1107
1108 let mut fused = FlushSink::default();
1109 handler.render_streaming(&protocol, &state, &render_options, &mut fused)?;
1110
1111 assert_eq!(fused.output, split.output, "bytes must match");
1112 assert_eq!(fused.flushes, split.flushes, "flush positions must match");
1113 Ok(())
1114 }
1115
1116 #[test]
1117 fn resume_overlays_new_state_and_reuses_unchanged_subtrees() -> Result<()> {
1118 let protocol = boundary_protocol(2, StateProjectionMode::All);
1122 let handler = WebUIHandler::new();
1123 let state = large_state(64);
1124 let render_options = options();
1125 let mut sink = TestSink {
1126 output: String::new(),
1127 };
1128 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1129
1130 let status = response.start(&state)?;
1131 let snapshot = rows_address(&response.core.frozen_state);
1132 let Some(boundary) = status.boundary.as_ref() else {
1133 panic!("the first occurrence must suspend");
1134 };
1135
1136 let mut next = state.clone();
1137 if let Some(object) = next.as_object_mut() {
1138 object.insert("title".to_string(), Value::String("second".to_string()));
1139 object.remove("count");
1140 }
1141 let status = response.resume(boundary.instance_id, &next, BoundaryMode::Final)?;
1142
1143 assert_eq!(
1144 response.core.frozen_state.get("title"),
1145 Some(&Value::String("second".to_string())),
1146 "a changed key must land in the snapshot"
1147 );
1148 assert_eq!(
1149 response.core.frozen_state.get("count"),
1150 Some(&test_json!(42)),
1151 "an omitted key keeps the value the snapshot already holds"
1152 );
1153 assert_eq!(
1154 rows_address(&response.core.frozen_state),
1155 snapshot,
1156 "an unchanged subtree must not be copied again"
1157 );
1158 assert!(
1159 status.boundary.is_none() && !status.done,
1160 "the commit step stops at its checkpoint"
1161 );
1162 assert!(
1163 response.advance()?.boundary.is_some(),
1164 "the second occurrence follows the parent bytes"
1165 );
1166 Ok(())
1167 }
1168
1169 #[test]
1170 fn semantic_steps_reuse_projection_scratch() -> Result<()> {
1171 let protocol = boundary_protocol(6, StateProjectionMode::Keys);
1175 let handler = WebUIHandler::new();
1176 let state = test_json!({ "count": 1, "title": "scratch", "unused": "x" });
1177 let render_options = options();
1178 let mut sink = TestSink {
1179 output: String::new(),
1180 };
1181 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1182
1183 let mut status = response.start(&state)?;
1184 let mut retained: Option<(usize, usize)> = None;
1185 while !status.done {
1186 let Some(boundary) = status.boundary.as_ref() else {
1187 status = response.advance()?;
1188 continue;
1189 };
1190 status = response.resume(boundary.instance_id, &state, BoundaryMode::Updatable)?;
1191 let Some(progress) = response.core.streaming.as_ref() else {
1192 panic!("a suspended response must retain its progress");
1193 };
1194 let observed = (
1195 progress.state_key_ids.capacity(),
1196 progress.state_key_ids.as_ptr().addr(),
1197 );
1198 assert!(
1199 observed.0 > 0,
1200 "the projection scratch must survive the record that filled it"
1201 );
1202 match retained {
1203 None => retained = Some(observed),
1204 Some(previous) => assert_eq!(
1205 observed, previous,
1206 "a later step must reuse the projection buffer, not allocate a new one"
1207 ),
1208 }
1209 }
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn updates_reuse_their_committed_projection_buffer() -> Result<()> {
1215 let protocol = boundary_protocol(2, StateProjectionMode::Keys);
1219 let handler = WebUIHandler::new();
1220 let state = test_json!({ "count": 1, "title": "updates" });
1221 let render_options = options();
1222 let mut sink = TestSink {
1223 output: String::new(),
1224 };
1225 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1226
1227 let status = response.start(&state)?;
1228 let Some(boundary) = status.boundary.as_ref() else {
1229 panic!("the first occurrence must suspend");
1230 };
1231 let instance_id = boundary.instance_id;
1232 response.resume(instance_id, &state, BoundaryMode::Updatable)?;
1233
1234 let mut retained: Option<(usize, usize)> = None;
1235 for _ in 0..4 {
1236 response.update(instance_id, &state)?;
1237 let Some(progress) = response.core.streaming.as_ref() else {
1238 panic!("a live response must retain its progress");
1239 };
1240 let Some(Some(plan)) = progress.update_plans.first() else {
1241 panic!("an updatable occurrence must retain its projection plan");
1242 };
1243 let observed = (plan.key_ids.capacity(), plan.key_ids.as_ptr().addr());
1244 assert!(!plan.requires_full_state, "keyed islands project keys");
1245 assert!(observed.0 > 0, "the plan must retain its key buffer");
1246 match retained {
1247 None => retained = Some(observed),
1248 Some(previous) => assert_eq!(
1249 observed, previous,
1250 "every update must reuse the committed projection buffer"
1251 ),
1252 }
1253 }
1254 assert_eq!(
1255 sink.output.matches(",2,0,{").count(),
1256 4,
1257 "each update emits exactly one typed state-update record"
1258 );
1259 Ok(())
1260 }
1261
1262 fn commit_occurrences<W: FlushWriter + ?Sized>(
1265 response: &mut StreamingResponse<'_, W>,
1266 status: StreamStatus,
1267 state: &Value,
1268 count: usize,
1269 mode: BoundaryMode,
1270 ) -> Result<StreamStatus> {
1271 let mut status = status;
1272 for committed in 0..count {
1273 let Some(boundary) = status.boundary.as_ref() else {
1274 panic!("occurrence {committed} must suspend before it can commit");
1275 };
1276 let instance_id = boundary.instance_id;
1277 response.resume(instance_id, state, mode)?;
1278 status = response.advance()?;
1279 }
1280 Ok(status)
1281 }
1282
1283 #[test]
1284 fn updatable_commits_stop_at_the_browser_retention_cap() -> Result<()> {
1285 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 1, StateProjectionMode::Keys);
1291 let handler = WebUIHandler::new();
1292 let state = test_json!({ "count": 1, "title": "retained" });
1293 let render_options = options();
1294 let mut sink = SharedSink::default();
1295 let written = SharedSink::clone(&sink).0;
1296 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1297
1298 let status = response.start(&state)?;
1299 let status = commit_occurrences(
1300 &mut response,
1301 status,
1302 &state,
1303 MAX_UPDATABLE_OCCURRENCES,
1304 BoundaryMode::Updatable,
1305 )?;
1306
1307 let Some(boundary) = status.boundary.as_ref() else {
1308 panic!("the occurrence past the cap must suspend like any other");
1309 };
1310 let instance_id = boundary.instance_id;
1311 let bytes_before = written.borrow().len();
1312 let refused = test_json!({ "count": 2, "title": "refused" });
1313 let rejected = response.resume(instance_id, &refused, BoundaryMode::Updatable);
1314 match rejected {
1315 Err(HandlerError::StreamingBoundary(error)) => {
1316 assert_eq!(error.signal, "resume");
1317 assert!(
1318 error.reason.contains("BoundaryMode::Final"),
1319 "the refusal must name the recovery: {}",
1320 error.reason
1321 );
1322 }
1323 _ => panic!("committing past the cap must fail with a typed boundary error"),
1324 }
1325 assert_eq!(
1326 written.borrow().len(),
1327 bytes_before,
1328 "a refused commit must not write a byte"
1329 );
1330 assert_eq!(
1331 response.core.frozen_state.get("title"),
1332 Some(&Value::String("retained".to_string())),
1333 "a refused commit must not merge its state into the snapshot"
1334 );
1335
1336 let status = response.resume(instance_id, &state, BoundaryMode::Final)?;
1338 assert!(
1339 status.boundary.is_none() && !status.done,
1340 "the retried commit stops at its own checkpoint"
1341 );
1342 assert!(
1343 written.borrow().len() > bytes_before,
1344 "the retry writes its checkpoint"
1345 );
1346 assert!(
1347 response.advance()?.done,
1348 "the response still reaches its terminal"
1349 );
1350 Ok(())
1351 }
1352
1353 #[test]
1354 fn final_commits_do_not_consume_the_updatable_cap() -> Result<()> {
1355 let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 2, StateProjectionMode::Keys);
1359 let handler = WebUIHandler::new();
1360 let state = test_json!({ "count": 1, "title": "mixed" });
1361 let render_options = options();
1362 let mut sink = TestSink {
1363 output: String::new(),
1364 };
1365 let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1366
1367 let status = response.start(&state)?;
1368 let status = commit_occurrences(&mut response, status, &state, 2, BoundaryMode::Final)?;
1369 let status = commit_occurrences(
1370 &mut response,
1371 status,
1372 &state,
1373 MAX_UPDATABLE_OCCURRENCES,
1374 BoundaryMode::Updatable,
1375 )?;
1376 assert!(
1377 status.done && response.is_done(),
1378 "final commits must leave the whole updatable budget available"
1379 );
1380 Ok(())
1381 }
1382
1383 #[test]
1384 fn owned_sessions_enforce_the_same_updatable_cap() -> Result<()> {
1385 let protocol = Arc::new(boundary_protocol(
1388 MAX_UPDATABLE_OCCURRENCES + 1,
1389 StateProjectionMode::Keys,
1390 ));
1391 let mut session = crate::streaming::StreamingSession::new(
1392 Arc::new(WebUIHandler::new()),
1393 protocol,
1394 crate::streaming::SessionOptions::new("index.html", "/"),
1395 )?;
1396 let state = test_json!({ "count": 1, "title": "owned" });
1397
1398 let mut step = session.start(&state)?;
1399 for committed in 0..MAX_UPDATABLE_OCCURRENCES {
1400 let Some(boundary) = step.boundary.as_ref() else {
1401 panic!("occurrence {committed} must suspend before it can commit");
1402 };
1403 let instance_id = boundary.instance_id;
1404 session.resume(instance_id, &state, BoundaryMode::Updatable)?;
1405 step = session.advance()?;
1406 }
1407
1408 let Some(boundary) = step.boundary.as_ref() else {
1409 panic!("the occurrence past the cap must suspend like any other");
1410 };
1411 let instance_id = boundary.instance_id;
1412 assert!(
1413 session
1414 .resume(instance_id, &state, BoundaryMode::Updatable)
1415 .is_err(),
1416 "an owned session refuses the occurrence past the cap"
1417 );
1418 let step = session.resume(instance_id, &state, BoundaryMode::Final)?;
1419 assert!(
1420 !step.bytes.is_empty(),
1421 "the retried commit still delivers its checkpoint bytes"
1422 );
1423 assert!(
1424 session.advance()?.done,
1425 "the owned response still reaches its terminal"
1426 );
1427 Ok(())
1428 }
1429}