Skip to main content

webui_handler/streaming/
session.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Public borrowed session API and shared owned continuation state.
5
6use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8use std::sync::Arc;
9
10use serde_json::{Number, Value};
11
12use super::error::{boundary_order_error, state_update_type_error};
13use super::state::{
14    increment_state_revision, increment_streaming_record_sequence, overlay_full_state,
15    overlay_full_state_owned, overlay_selected_state, overlay_selected_state_owned,
16    selected_state_snapshot, selected_state_snapshot_owned, StreamingProgress,
17    StreamingRenderState,
18};
19use super::vm::{ContinuationVm, StepGoal};
20use super::StreamingSink;
21use crate::plugin::HandlerPlugin;
22use crate::route_handler::Protocol;
23use crate::{
24    FlushWriter, HandlerError, RenderOptions, ResponseWriter, Result, WebUIHandler,
25    WebUIProcessContext,
26};
27
28/// Maximum continuation frames retained by one response.
29pub const MAX_CONTINUATION_DEPTH: usize = 256;
30/// Maximum nested unfinished component spans.
31pub const MAX_SPAN_NESTING: usize = 32;
32/// Maximum runtime boundary occurrences in one response.
33pub const MAX_BOUNDARY_OCCURRENCES: usize = 512;
34/// Maximum runtime boundary occurrences one response may commit as
35/// [`BoundaryMode::Updatable`].
36///
37/// Mirrors the browser coordinator's retained-boundary cap: the client refuses
38/// to retain a 129th updatable occurrence, so the server refuses to emit one
39/// rather than stream a checkpoint the page would fail on.
40pub(crate) const MAX_UPDATABLE_OCCURRENCES: usize = 128;
41/// Maximum keyed runtime occurrences tracked for uniqueness.
42pub const MAX_KEYED_INSTANCES: usize = 512;
43/// Maximum top-level state keys retained by a continuation snapshot.
44pub(crate) const MAX_FROZEN_STATE_KEYS: usize = 1_024;
45
46/// Response-local runtime boundary occurrence identity.
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub struct BoundaryInstanceId(u32);
49
50impl BoundaryInstanceId {
51    /// Rebuild an ID round-tripped through a host binding.
52    #[must_use]
53    pub const fn from_raw(raw: u32) -> Self {
54        Self(raw)
55    }
56
57    /// Return the wire integer.
58    #[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/// Response-local generated component span identity.
71#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
72pub struct SpanInstanceId(u32);
73
74impl SpanInstanceId {
75    /// Rebuild an ID round-tripped through a host binding.
76    #[must_use]
77    pub const fn from_raw(raw: u32) -> Self {
78        Self(raw)
79    }
80
81    /// Return the wire integer.
82    #[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/// Valid evaluated key for a repeated boundary declaration.
93#[derive(Clone, Debug, Eq, Hash, PartialEq)]
94pub enum BoundaryKey {
95    /// Authored key resolved to a JSON string.
96    String(String),
97    /// Authored key resolved to a finite JSON number.
98    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
116/// Borrowed or owned JSON state supplied to a streaming step.
117///
118/// `Value`, `&Value`, and references to smart pointers such as `&Arc<Value>`
119/// convert implicitly, so callers use one method name without losing ownership
120/// control.
121pub enum StreamingState<'a> {
122    /// State retained by the caller; the session clones only its projection.
123    Borrowed(&'a Value),
124    /// State transferred to the session so changed subtrees can move.
125    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/// Runtime occurrence returned when traversal suspends.
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct BoundaryDescriptor {
155    /// Gapless response-local occurrence ID.
156    pub instance_id: BoundaryInstanceId,
157    /// Stable compiler declaration ID.
158    pub declaration_id: u32,
159    /// Entry or component template that owns the declaration.
160    ///
161    /// Interned per protocol, so producing a descriptor shares the compiled
162    /// string instead of allocating a copy per occurrence.
163    pub owner: Arc<str>,
164    /// Free-form authored declaration name.
165    ///
166    /// Interned per protocol alongside [`Self::owner`].
167    pub name: Arc<str>,
168    /// Evaluated repeat key, when authored.
169    pub key: Option<BoundaryKey>,
170}
171
172/// Whether a committed occurrence may receive later state updates.
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum BoundaryMode {
175    /// Hydrate once and release boundary-local roots.
176    Final,
177    /// Retain live roots until terminal for later [`StreamingResponse::update`].
178    Updatable,
179}
180
181/// Borrowed-writer result of one semantic streaming step.
182///
183/// The pair of fields names exactly one state:
184///
185/// | `boundary` | `done` | meaning |
186/// |------------|--------|---------|
187/// | `Some`     | `false`| the occurrence is waiting for [`StreamingResponse::resume`] |
188/// | `None`     | `false`| the committed occurrence flushed; call [`StreamingResponse::advance`] |
189/// | `None`     | `true` | the terminal record and writer end completed |
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct StreamStatus {
192    /// The next occurrence waiting for [`StreamingResponse::resume`].
193    pub boundary: Option<BoundaryDescriptor>,
194    /// True after the terminal record and writer end completed.
195    pub done: bool,
196}
197
198/// A progressive response that writes directly through a [`FlushWriter`].
199///
200/// Steps alternate: [`Self::start`] writes the shell prefix and stops before
201/// the first occurrence, [`Self::resume`] writes exactly one occurrence through
202/// its checkpoint, and [`Self::advance`] writes the ordinary parent bytes that
203/// follow it until the next occurrence or the terminal. Every step is one
204/// independently writable, independently flushed segment.
205pub 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    /// Create a runtime-discovered progressive response.
215    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    /// Render until the first runtime boundary occurrence or terminal.
246    ///
247    /// Pass an owned [`Value`] to move it into the continuation, or a reference
248    /// to retain caller ownership and clone only the required projection.
249    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    /// Commit the pending occurrence through its checkpoint, then stop.
261    ///
262    /// The bytes written by this call are exactly the occurrence's own record —
263    /// no parent or tail bytes follow it — so the host can release the
264    /// occurrence the moment it resolves. Call [`Self::advance`] next.
265    ///
266    /// [`BoundaryMode::Updatable`] is refused once the response has committed
267    /// as many updatable occurrences as the browser retains. The refusal is
268    /// raised before any byte or state moves, so the same occurrence stays
269    /// pending and can be committed with [`BoundaryMode::Final`] instead.
270    /// Pass an owned [`Value`] to move changed subtrees, or a reference to retain
271    /// caller ownership. Use [`Self::resume_current`] when no state changed.
272    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    /// Commit the pending occurrence against the retained state snapshot.
286    ///
287    /// This preserves the checkpoint flush and async pause point while avoiding
288    /// an overlay when no top-level state changed since the preceding step.
289    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    /// Write the ordinary parent bytes that follow a committed occurrence.
299    ///
300    /// Valid only after [`Self::resume`]. Renders the shell until the next
301    /// occurrence suspends or the terminal record completes.
302    pub fn advance(&mut self) -> Result<StreamStatus> {
303        let (core, call) = self.parts();
304        core.advance(call)
305    }
306
307    /// Emit one projected state update for a committed updatable occurrence.
308    ///
309    /// Valid between [`Self::resume`] and [`Self::advance`] as well as after
310    /// the response has moved on, so a host can revise the occurrence it just
311    /// committed while the response is still open.
312    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    /// Whether the response has emitted its terminal and ended its writer.
323    #[must_use]
324    pub fn is_done(&self) -> bool {
325        self.core.done
326    }
327
328    /// Split the response into its continuation and one call's borrowed
329    /// runtime, so every entry point shares the same wiring.
330    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    /// True between a committed occurrence and the `advance` that writes the
359    /// parent bytes following it.
360    awaiting_advance: bool,
361    local_vars: HashMap<String, Value>,
362    component_attrs: HashMap<String, Value>,
363    route_base: Option<String>,
364    rendered_components: HashSet<String>,
365    document_style_resources: HashSet<String>,
366    shadow_style_roots: Vec<crate::ShadowStyleRoot>,
367    plugin: Option<Box<dyn HandlerPlugin>>,
368    route_children: Vec<webui_protocol::WebUiFragmentRoute>,
369    head_end_emitted: bool,
370    body_start_emitted: bool,
371    component_asset_styles_emitted: bool,
372    body_end_emitted: bool,
373    route_chain_index: usize,
374    route_chain: Option<Vec<crate::route_handler::RouteChainEntry>>,
375    route_document_style_targets: Vec<bool>,
376    reachable_components: Option<Vec<String>>,
377    streaming: Option<StreamingProgress>,
378    json_scratch: Vec<u8>,
379    scope_pool: Vec<HashMap<String, Value>>,
380}
381
382pub(crate) struct SessionCall<'call, 'data> {
383    pub(crate) handler: &'call WebUIHandler,
384    pub(crate) protocol: &'data Protocol,
385    pub(crate) options: &'call RenderOptions<'data>,
386    pub(crate) writer: &'call mut dyn ResponseWriter,
387}
388
389impl SessionCore {
390    pub(crate) fn new(handler: &WebUIHandler, protocol: &Protocol, entry_id: &str) -> Result<Self> {
391        if !protocol.protocol().fragments.contains_key(entry_id) {
392            return Err(HandlerError::MissingFragment(entry_id.to_string()));
393        }
394        let state_plan = protocol.continuation_state_plan(entry_id)?.resolve()?;
395        Ok(Self {
396            vm: ContinuationVm::new(entry_id, protocol)?,
397            frozen_keys: Arc::clone(&state_plan.keys),
398            requires_full_state: state_plan.requires_full_state,
399            frozen_state: Value::Object(serde_json::Map::new()),
400            started: false,
401            done: false,
402            failed: false,
403            awaiting_advance: false,
404            local_vars: HashMap::new(),
405            component_attrs: HashMap::new(),
406            route_base: None,
407            rendered_components: HashSet::new(),
408            document_style_resources: HashSet::new(),
409            shadow_style_roots: Vec::new(),
410            plugin: handler.plugin_factory.map(|factory| factory()),
411            route_children: 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        // The value is moved into each render context and back, so protocols
442        // requiring full projection pay for exactly one response-local clone.
443        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    /// Render a one-shot streaming response in one prepared process context.
529    ///
530    /// The synchronous helper has no async host handoff, so retaining the
531    /// caller's state borrow for the complete traversal avoids both a state clone
532    /// and per-boundary context teardown while preserving every transport flush.
533    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 = std::mem::take(&mut context.route_children);
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    /// Reject a `resume` that is out of order without writing a byte.
772    ///
773    /// Ordering is checked before any render work, so a rejected call leaves
774    /// the response exactly as it was and the host can retry with the right
775    /// instance ID or the missing `advance`.
776    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    /// Reject an `advance` that is out of order without writing a byte.
796    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    /// A sink whose bytes stay readable while the response holds it, so a test
854    /// can prove a refused step wrote nothing.
855    #[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    /// Build a parser-produced entry with `boundaries` runtime occurrences,
876    /// each hosting one island component.
877    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    /// A state whose payload is large enough that a per-boundary copy would be
926    /// unmistakable in both time and allocation.
927    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    /// Heap address of the retained snapshot's `rows` buffer, or `None` when
944    /// the snapshot does not hold it.
945    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 render_streaming_projects_full_state_once_per_response() -> Result<()> {
958        // Full-state protocols retain the caller's whole tree. Committing each
959        // occurrence against the snapshot the response already holds must not
960        // re-copy that tree, so the retained buffer keeps its identity for the
961        // entire response no matter how many boundaries commit.
962        let protocol = boundary_protocol(8, StateProjectionMode::All);
963        let handler = WebUIHandler::new();
964        let state = large_state(256);
965        let render_options = options();
966        let mut sink = TestSink {
967            output: String::new(),
968        };
969        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
970
971        let mut status = response.start(&state)?;
972        let snapshot = rows_address(&response.core.frozen_state);
973        assert!(
974            snapshot.is_some(),
975            "a full-state protocol must retain the caller's payload"
976        );
977        assert_ne!(
978            snapshot,
979            rows_address(&state),
980            "the response owns its snapshot rather than borrowing the caller's tree"
981        );
982
983        let mut committed = 0usize;
984        while !status.done {
985            status = match status.boundary.as_ref() {
986                Some(boundary) => {
987                    let next =
988                        response.resume(boundary.instance_id, &state, BoundaryMode::Final)?;
989                    committed += 1;
990                    assert!(
991                        next.boundary.is_none() && !next.done,
992                        "a commit step stops at its checkpoint and waits for advance"
993                    );
994                    assert_eq!(
995                        rows_address(&response.core.frozen_state),
996                        snapshot,
997                        "committing occurrence {committed} must not re-copy the retained snapshot"
998                    );
999                    next
1000                }
1001                None => response.advance()?,
1002            };
1003        }
1004        assert_eq!(committed, 8, "every authored boundary must commit");
1005
1006        // The one-shot helper fuses commit and advance into one continuation
1007        // setup, so its bytes must match the split steps it stands in for.
1008        let mut helper_sink = TestSink {
1009            output: String::new(),
1010        };
1011        handler.render_streaming(&protocol, &state, &render_options, &mut helper_sink)?;
1012        assert_eq!(
1013            helper_sink.output, sink.output,
1014            "render_streaming must resume against the retained snapshot"
1015        );
1016        Ok(())
1017    }
1018
1019    #[test]
1020    fn fused_helper_matches_split_step_bytes_and_flushes() -> Result<()> {
1021        // `render_streaming` skips the host hand-off between a commit and the
1022        // parent bytes that follow it. That is a scheduling shortcut only: the
1023        // emitted bytes and the positions at which they are flushed must match
1024        // the split step machine exactly.
1025        #[derive(Default)]
1026        struct FlushSink {
1027            output: String,
1028            flushes: Vec<usize>,
1029        }
1030
1031        impl ResponseWriter for FlushSink {
1032            fn write(&mut self, content: &str) -> Result<()> {
1033                self.output.push_str(content);
1034                Ok(())
1035            }
1036
1037            fn end(&mut self) -> Result<()> {
1038                Ok(())
1039            }
1040        }
1041
1042        impl FlushWriter for FlushSink {
1043            fn flush(&mut self) -> Result<()> {
1044                self.flushes.push(self.output.len());
1045                Ok(())
1046            }
1047        }
1048
1049        let protocol = boundary_protocol(4, StateProjectionMode::Keys);
1050        let handler = WebUIHandler::new();
1051        let state = test_json!({ "count": 3, "title": "fused" });
1052        let render_options = options();
1053
1054        let mut split = FlushSink::default();
1055        {
1056            let mut response = handler.stream_response(&protocol, &render_options, &mut split)?;
1057            let mut status = response.start(&state)?;
1058            while !status.done {
1059                status = match status.boundary.as_ref() {
1060                    Some(boundary) => {
1061                        response.resume(boundary.instance_id, &state, BoundaryMode::Final)?
1062                    }
1063                    None => response.advance()?,
1064                };
1065            }
1066        }
1067
1068        let mut fused = FlushSink::default();
1069        handler.render_streaming(&protocol, &state, &render_options, &mut fused)?;
1070
1071        assert_eq!(fused.output, split.output, "bytes must match");
1072        assert_eq!(fused.flushes, split.flushes, "flush positions must match");
1073        Ok(())
1074    }
1075
1076    #[test]
1077    fn resume_overlays_new_state_and_reuses_unchanged_subtrees() -> Result<()> {
1078        // The public resume keeps its patch semantics: a changed key lands in
1079        // the snapshot, an omitted key survives, and an unchanged subtree is
1080        // left in place instead of being copied again.
1081        let protocol = boundary_protocol(2, StateProjectionMode::All);
1082        let handler = WebUIHandler::new();
1083        let state = large_state(64);
1084        let render_options = options();
1085        let mut sink = TestSink {
1086            output: String::new(),
1087        };
1088        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1089
1090        let status = response.start(&state)?;
1091        let snapshot = rows_address(&response.core.frozen_state);
1092        let Some(boundary) = status.boundary.as_ref() else {
1093            panic!("the first occurrence must suspend");
1094        };
1095
1096        let mut next = state.clone();
1097        if let Some(object) = next.as_object_mut() {
1098            object.insert("title".to_string(), Value::String("second".to_string()));
1099            object.remove("count");
1100        }
1101        let status = response.resume(boundary.instance_id, &next, BoundaryMode::Final)?;
1102
1103        assert_eq!(
1104            response.core.frozen_state.get("title"),
1105            Some(&Value::String("second".to_string())),
1106            "a changed key must land in the snapshot"
1107        );
1108        assert_eq!(
1109            response.core.frozen_state.get("count"),
1110            Some(&test_json!(42)),
1111            "an omitted key keeps the value the snapshot already holds"
1112        );
1113        assert_eq!(
1114            rows_address(&response.core.frozen_state),
1115            snapshot,
1116            "an unchanged subtree must not be copied again"
1117        );
1118        assert!(
1119            status.boundary.is_none() && !status.done,
1120            "the commit step stops at its checkpoint"
1121        );
1122        assert!(
1123            response.advance()?.boundary.is_some(),
1124            "the second occurrence follows the parent bytes"
1125        );
1126        Ok(())
1127    }
1128
1129    #[test]
1130    fn semantic_steps_reuse_projection_scratch() -> Result<()> {
1131        // The record projection scratch lives in the retained progress as plain
1132        // integers, so after the first record every later step reuses the same
1133        // allocation instead of rebuilding one per step.
1134        let protocol = boundary_protocol(6, StateProjectionMode::Keys);
1135        let handler = WebUIHandler::new();
1136        let state = test_json!({ "count": 1, "title": "scratch", "unused": "x" });
1137        let render_options = options();
1138        let mut sink = TestSink {
1139            output: String::new(),
1140        };
1141        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1142
1143        let mut status = response.start(&state)?;
1144        let mut retained: Option<(usize, usize)> = None;
1145        while !status.done {
1146            let Some(boundary) = status.boundary.as_ref() else {
1147                status = response.advance()?;
1148                continue;
1149            };
1150            status = response.resume(boundary.instance_id, &state, BoundaryMode::Updatable)?;
1151            let Some(progress) = response.core.streaming.as_ref() else {
1152                panic!("a suspended response must retain its progress");
1153            };
1154            let observed = (
1155                progress.state_key_ids.capacity(),
1156                progress.state_key_ids.as_ptr().addr(),
1157            );
1158            assert!(
1159                observed.0 > 0,
1160                "the projection scratch must survive the record that filled it"
1161            );
1162            match retained {
1163                None => retained = Some(observed),
1164                Some(previous) => assert_eq!(
1165                    observed, previous,
1166                    "a later step must reuse the projection buffer, not allocate a new one"
1167                ),
1168            }
1169        }
1170        Ok(())
1171    }
1172
1173    #[test]
1174    fn updates_reuse_their_committed_projection_buffer() -> Result<()> {
1175        // An update writes through the plan captured at commit time: no key
1176        // list is rebuilt, so the plan's buffer keeps its identity across every
1177        // update it serves.
1178        let protocol = boundary_protocol(2, StateProjectionMode::Keys);
1179        let handler = WebUIHandler::new();
1180        let state = test_json!({ "count": 1, "title": "updates" });
1181        let render_options = options();
1182        let mut sink = TestSink {
1183            output: String::new(),
1184        };
1185        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1186
1187        let status = response.start(&state)?;
1188        let Some(boundary) = status.boundary.as_ref() else {
1189            panic!("the first occurrence must suspend");
1190        };
1191        let instance_id = boundary.instance_id;
1192        response.resume(instance_id, &state, BoundaryMode::Updatable)?;
1193
1194        let mut retained: Option<(usize, usize)> = None;
1195        for _ in 0..4 {
1196            response.update(instance_id, &state)?;
1197            let Some(progress) = response.core.streaming.as_ref() else {
1198                panic!("a live response must retain its progress");
1199            };
1200            let Some(Some(plan)) = progress.update_plans.first() else {
1201                panic!("an updatable occurrence must retain its projection plan");
1202            };
1203            let observed = (plan.key_ids.capacity(), plan.key_ids.as_ptr().addr());
1204            assert!(!plan.requires_full_state, "keyed islands project keys");
1205            assert!(observed.0 > 0, "the plan must retain its key buffer");
1206            match retained {
1207                None => retained = Some(observed),
1208                Some(previous) => assert_eq!(
1209                    observed, previous,
1210                    "every update must reuse the committed projection buffer"
1211                ),
1212            }
1213        }
1214        assert_eq!(
1215            sink.output.matches(",2,0,{").count(),
1216            4,
1217            "each update emits exactly one typed state-update record"
1218        );
1219        Ok(())
1220    }
1221
1222    /// Drive `count` occurrences to completion in `mode`, returning the status
1223    /// the response stopped on.
1224    fn commit_occurrences<W: FlushWriter + ?Sized>(
1225        response: &mut StreamingResponse<'_, W>,
1226        status: StreamStatus,
1227        state: &Value,
1228        count: usize,
1229        mode: BoundaryMode,
1230    ) -> Result<StreamStatus> {
1231        let mut status = status;
1232        for committed in 0..count {
1233            let Some(boundary) = status.boundary.as_ref() else {
1234                panic!("occurrence {committed} must suspend before it can commit");
1235            };
1236            let instance_id = boundary.instance_id;
1237            response.resume(instance_id, state, mode)?;
1238            status = response.advance()?;
1239        }
1240        Ok(status)
1241    }
1242
1243    #[test]
1244    fn updatable_commits_stop_at_the_browser_retention_cap() -> Result<()> {
1245        // The browser retains every updatable occurrence for the life of the
1246        // response and refuses the one past its cap, so the server refuses to
1247        // emit that checkpoint at all. The refusal lands before a byte is
1248        // written and before the caller's state reaches the snapshot, leaving
1249        // the same occurrence pending so the host can commit it as final.
1250        let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 1, StateProjectionMode::Keys);
1251        let handler = WebUIHandler::new();
1252        let state = test_json!({ "count": 1, "title": "retained" });
1253        let render_options = options();
1254        let mut sink = SharedSink::default();
1255        let written = SharedSink::clone(&sink).0;
1256        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1257
1258        let status = response.start(&state)?;
1259        let status = commit_occurrences(
1260            &mut response,
1261            status,
1262            &state,
1263            MAX_UPDATABLE_OCCURRENCES,
1264            BoundaryMode::Updatable,
1265        )?;
1266
1267        let Some(boundary) = status.boundary.as_ref() else {
1268            panic!("the occurrence past the cap must suspend like any other");
1269        };
1270        let instance_id = boundary.instance_id;
1271        let bytes_before = written.borrow().len();
1272        let refused = test_json!({ "count": 2, "title": "refused" });
1273        let rejected = response.resume(instance_id, &refused, BoundaryMode::Updatable);
1274        match rejected {
1275            Err(HandlerError::StreamingBoundary(error)) => {
1276                assert_eq!(error.signal, "resume");
1277                assert!(
1278                    error.reason.contains("BoundaryMode::Final"),
1279                    "the refusal must name the recovery: {}",
1280                    error.reason
1281                );
1282            }
1283            _ => panic!("committing past the cap must fail with a typed boundary error"),
1284        }
1285        assert_eq!(
1286            written.borrow().len(),
1287            bytes_before,
1288            "a refused commit must not write a byte"
1289        );
1290        assert_eq!(
1291            response.core.frozen_state.get("title"),
1292            Some(&Value::String("retained".to_string())),
1293            "a refused commit must not merge its state into the snapshot"
1294        );
1295
1296        // The occurrence is untouched, so the same ID commits as final.
1297        let status = response.resume(instance_id, &state, BoundaryMode::Final)?;
1298        assert!(
1299            status.boundary.is_none() && !status.done,
1300            "the retried commit stops at its own checkpoint"
1301        );
1302        assert!(
1303            written.borrow().len() > bytes_before,
1304            "the retry writes its checkpoint"
1305        );
1306        assert!(
1307            response.advance()?.done,
1308            "the response still reaches its terminal"
1309        );
1310        Ok(())
1311    }
1312
1313    #[test]
1314    fn final_commits_do_not_consume_the_updatable_cap() -> Result<()> {
1315        // Only occurrences the browser retains count against the cap: a final
1316        // boundary releases its roots at hydration, so a response may commit
1317        // any number of them and still use its full updatable budget.
1318        let protocol = boundary_protocol(MAX_UPDATABLE_OCCURRENCES + 2, StateProjectionMode::Keys);
1319        let handler = WebUIHandler::new();
1320        let state = test_json!({ "count": 1, "title": "mixed" });
1321        let render_options = options();
1322        let mut sink = TestSink {
1323            output: String::new(),
1324        };
1325        let mut response = handler.stream_response(&protocol, &render_options, &mut sink)?;
1326
1327        let status = response.start(&state)?;
1328        let status = commit_occurrences(&mut response, status, &state, 2, BoundaryMode::Final)?;
1329        let status = commit_occurrences(
1330            &mut response,
1331            status,
1332            &state,
1333            MAX_UPDATABLE_OCCURRENCES,
1334            BoundaryMode::Updatable,
1335        )?;
1336        assert!(
1337            status.done && response.is_done(),
1338            "final commits must leave the whole updatable budget available"
1339        );
1340        Ok(())
1341    }
1342
1343    #[test]
1344    fn owned_sessions_enforce_the_same_updatable_cap() -> Result<()> {
1345        // The owned session shares the borrowed session's continuation, so the
1346        // cap, the refusal, and the final-mode retry behave identically.
1347        let protocol = Arc::new(boundary_protocol(
1348            MAX_UPDATABLE_OCCURRENCES + 1,
1349            StateProjectionMode::Keys,
1350        ));
1351        let mut session = crate::streaming::StreamingSession::new(
1352            Arc::new(WebUIHandler::new()),
1353            protocol,
1354            crate::streaming::SessionOptions::new("index.html", "/"),
1355        )?;
1356        let state = test_json!({ "count": 1, "title": "owned" });
1357
1358        let mut step = session.start(&state)?;
1359        for committed in 0..MAX_UPDATABLE_OCCURRENCES {
1360            let Some(boundary) = step.boundary.as_ref() else {
1361                panic!("occurrence {committed} must suspend before it can commit");
1362            };
1363            let instance_id = boundary.instance_id;
1364            session.resume(instance_id, &state, BoundaryMode::Updatable)?;
1365            step = session.advance()?;
1366        }
1367
1368        let Some(boundary) = step.boundary.as_ref() else {
1369            panic!("the occurrence past the cap must suspend like any other");
1370        };
1371        let instance_id = boundary.instance_id;
1372        assert!(
1373            session
1374                .resume(instance_id, &state, BoundaryMode::Updatable)
1375                .is_err(),
1376            "an owned session refuses the occurrence past the cap"
1377        );
1378        let step = session.resume(instance_id, &state, BoundaryMode::Final)?;
1379        assert!(
1380            !step.bytes.is_empty(),
1381            "the retried commit still delivers its checkpoint bytes"
1382        );
1383        assert!(
1384            session.advance()?.done,
1385            "the owned response still reaches its terminal"
1386        );
1387        Ok(())
1388    }
1389}