Skip to main content

webui_handler/streaming/
owned.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Host-owned progressive sessions with pull-based byte delivery.
5
6use std::sync::Arc;
7
8use serde_json::Value;
9
10use super::session::{
11    BoundaryDescriptor, BoundaryInstanceId, BoundaryMode, SessionCall, SessionCore, StreamStatus,
12};
13use super::StreamingSink;
14use crate::route_handler::Protocol;
15use crate::{FlushWriter, RenderOptions, ResponseWriter, Result, WebUIHandler};
16
17/// Reusable byte sink for host-owned sessions.
18///
19/// Records where the streaming transport flushed so a session can prove each
20/// returned step ends on a real flush boundary rather than silently merging a
21/// checkpoint with the bytes that follow it.
22#[derive(Default)]
23pub struct BufferSink {
24    bytes: Vec<u8>,
25    last_flush: usize,
26}
27
28impl BufferSink {
29    fn reset(&mut self) {
30        self.bytes.clear();
31        self.last_flush = 0;
32    }
33}
34
35impl ResponseWriter for BufferSink {
36    fn write(&mut self, content: &str) -> Result<()> {
37        self.bytes.extend_from_slice(content.as_bytes());
38        Ok(())
39    }
40
41    fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
42        crate::append_attribute_to_bytes(&mut self.bytes, name, value);
43        Ok(())
44    }
45
46    fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
47        crate::append_boolean_attribute_to_bytes(&mut self.bytes, name);
48        Ok(())
49    }
50
51    fn end(&mut self) -> Result<()> {
52        Ok(())
53    }
54}
55
56impl FlushWriter for BufferSink {
57    fn flush(&mut self) -> Result<()> {
58        self.last_flush = self.bytes.len();
59        Ok(())
60    }
61}
62
63/// Owned per-response configuration.
64#[derive(Clone, Debug)]
65pub struct SessionOptions {
66    /// Entry fragment to render.
67    pub entry_id: String,
68    /// Request path used for route matching.
69    pub request_path: String,
70    /// Optional CSP nonce.
71    pub nonce: Option<String>,
72    /// Optional trusted HTML injected at head_end.
73    pub head_inject: Option<String>,
74    /// Optional trusted HTML injected at body_end.
75    pub body_inject: Option<String>,
76}
77
78impl SessionOptions {
79    /// Create options for an entry and request path.
80    #[must_use]
81    pub fn new(entry_id: impl Into<String>, request_path: impl Into<String>) -> Self {
82        Self {
83            entry_id: entry_id.into(),
84            request_path: request_path.into(),
85            nonce: None,
86            head_inject: None,
87            body_inject: None,
88        }
89    }
90}
91
92/// Bytes and continuation state returned by one owned semantic step.
93///
94/// The bytes are one semantic write segment ending on a transport flush
95/// boundary: the shell prefix, exactly one committed occurrence, the parent
96/// bytes between two occurrences, or the tail plus terminal. A host writes them
97/// with a single `write` + `flush`.
98#[derive(Clone, Debug, Eq, PartialEq)]
99pub struct StreamStep {
100    /// Complete bytes produced by this call.
101    pub bytes: Vec<u8>,
102    /// Next runtime occurrence waiting for resume.
103    pub boundary: Option<BoundaryDescriptor>,
104    /// True after terminal emission.
105    pub done: bool,
106}
107
108/// A progressive response owned independently of any Rust borrow.
109///
110/// Exposes the same step machine as [`super::StreamingResponse`]:
111/// [`Self::start`] writes the shell prefix, [`Self::resume`] writes exactly one
112/// occurrence through its checkpoint, and [`Self::advance`] writes the parent
113/// bytes that follow it.
114pub struct StreamingSession {
115    handler: Arc<WebUIHandler>,
116    protocol: Arc<Protocol>,
117    options: SessionOptions,
118    core: SessionCore,
119    sink: BufferSink,
120}
121
122impl StreamingSession {
123    /// Create an unstarted owned session.
124    pub fn new(
125        handler: Arc<WebUIHandler>,
126        protocol: Arc<Protocol>,
127        options: SessionOptions,
128    ) -> Result<Self> {
129        let core = SessionCore::new(&handler, &protocol, &options.entry_id)?;
130        Ok(Self {
131            handler,
132            protocol,
133            options,
134            core,
135            sink: BufferSink::default(),
136        })
137    }
138
139    /// Render until the first runtime boundary occurrence or terminal.
140    pub fn start(&mut self, state: &Value) -> Result<StreamStep> {
141        self.step(|core, call| core.start(call, state))
142    }
143
144    /// Commit the pending occurrence through its checkpoint, then stop.
145    ///
146    /// The returned bytes hold that occurrence's record and nothing that
147    /// follows it. Call [`Self::advance`] for the parent bytes.
148    ///
149    /// [`BoundaryMode::Updatable`] is refused once the response has committed
150    /// as many updatable occurrences as the browser retains. The refusal
151    /// produces no bytes and leaves the occurrence pending, so it can be
152    /// committed with [`BoundaryMode::Final`] instead.
153    pub fn resume(
154        &mut self,
155        instance_id: BoundaryInstanceId,
156        state: &Value,
157        mode: BoundaryMode,
158    ) -> Result<StreamStep> {
159        self.step(|core, call| core.resume(call, instance_id, state, mode))
160    }
161
162    /// Write the ordinary parent bytes that follow a committed occurrence.
163    ///
164    /// Valid only after [`Self::resume`].
165    pub fn advance(&mut self) -> Result<StreamStep> {
166        self.step(SessionCore::advance)
167    }
168
169    /// Emit an update for one committed updatable runtime occurrence.
170    ///
171    /// Valid between [`Self::resume`] and [`Self::advance`], so a host can
172    /// revise the occurrence it just committed while the response stays open.
173    pub fn update(&mut self, instance_id: BoundaryInstanceId, patch: &Value) -> Result<Vec<u8>> {
174        self.sink.reset();
175        let options = render_options(&self.options);
176        let mut sink = StreamingSink {
177            transport: &mut self.sink,
178            component_opening: None,
179            written: 0,
180            flushed: 0,
181        };
182        let result = self.core.update(
183            SessionCall {
184                handler: &self.handler,
185                protocol: &self.protocol,
186                options: &options,
187                writer: &mut sink,
188            },
189            instance_id,
190            patch,
191        );
192        match result {
193            Ok(()) => {
194                verify_flush_boundary(&self.sink)?;
195                Ok(std::mem::take(&mut self.sink.bytes))
196            }
197            Err(error) => {
198                self.sink.reset();
199                Err(error)
200            }
201        }
202    }
203
204    /// Whether terminal emission completed.
205    #[must_use]
206    pub fn is_done(&self) -> bool {
207        self.core.done
208    }
209
210    fn step(
211        &mut self,
212        operation: impl FnOnce(&mut SessionCore, SessionCall<'_, '_>) -> Result<StreamStatus>,
213    ) -> Result<StreamStep> {
214        self.sink.reset();
215        let options = render_options(&self.options);
216        let mut sink = StreamingSink {
217            transport: &mut self.sink,
218            component_opening: None,
219            written: 0,
220            flushed: 0,
221        };
222        let status = operation(
223            &mut self.core,
224            SessionCall {
225                handler: &self.handler,
226                protocol: &self.protocol,
227                options: &options,
228                writer: &mut sink,
229            },
230        );
231        match status {
232            Ok(status) => {
233                verify_flush_boundary(&self.sink)?;
234                Ok(StreamStep {
235                    bytes: std::mem::take(&mut self.sink.bytes),
236                    boundary: status.boundary,
237                    done: status.done,
238                })
239            }
240            Err(error) => {
241                self.sink.reset();
242                Err(error)
243            }
244        }
245    }
246}
247
248/// Reject a step whose buffered bytes extend past the last transport flush.
249///
250/// A host-owned session hands the caller a byte buffer instead of a live
251/// transport, so a step that produced bytes after its checkpoint flushed would
252/// silently merge two semantic segments into one host write. The check is one
253/// integer compare per step.
254fn verify_flush_boundary(sink: &BufferSink) -> Result<()> {
255    if sink.last_flush == sink.bytes.len() {
256        return Ok(());
257    }
258    Err(unflushed_step_error())
259}
260
261#[cold]
262#[inline(never)]
263fn unflushed_step_error() -> crate::HandlerError {
264    crate::HandlerError::Invariant(
265        "a streaming step buffered bytes past its last flush boundary".to_string(),
266    )
267}
268
269fn render_options(options: &SessionOptions) -> RenderOptions<'_> {
270    RenderOptions {
271        entry_id: &options.entry_id,
272        request_path: &options.request_path,
273        nonce: options.nonce.as_deref(),
274        head_inject: options.head_inject.as_deref(),
275        body_inject: options.body_inject.as_deref(),
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use std::sync::{Arc, Mutex};
282
283    use serde_json::Value;
284
285    use super::{BufferSink, SessionOptions, StreamingSession};
286    use crate::route_handler::Protocol;
287    use crate::{BoundaryMode, FlushWriter, ResponseWriter, WebUIHandler};
288
289    #[test]
290    fn owned_streaming_session_supports_synchronized_hosts() {
291        fn assert_send<T: Send>() {}
292        fn assert_send_sync<T: Send + Sync>() {}
293
294        assert_send::<StreamingSession>();
295        assert_send_sync::<Mutex<StreamingSession>>();
296        assert_send_sync::<Arc<WebUIHandler>>();
297    }
298
299    #[test]
300    fn buffer_sink_tracks_the_transport_flush_boundary() {
301        // The invariant every owned step is checked against: bytes written
302        // after the last flush would merge two semantic segments into one host
303        // write.
304        let mut sink = BufferSink::default();
305        sink.write("checkpoint").expect("write");
306        sink.flush().expect("flush");
307        assert!(super::verify_flush_boundary(&sink).is_ok());
308        sink.write("tail").expect("write");
309        assert!(super::verify_flush_boundary(&sink).is_err());
310        sink.reset();
311        assert!(super::verify_flush_boundary(&sink).is_ok());
312    }
313
314    #[test]
315    fn every_owned_step_ends_on_a_flush_boundary() {
316        let mut parser = webui_parser::HtmlParser::new();
317        parser
318            .parse(
319                "index.html",
320                concat!(
321                    "<html><head></head><body>",
322                    r#"<boundary name="first"><p>1</p></boundary>"#,
323                    "<hr>",
324                    r#"<boundary name="second"><p>2</p></boundary>"#,
325                    "<footer>tail</footer></body></html>",
326                ),
327            )
328            .expect("parse");
329        let protocol = Arc::new(Protocol::new(webui_protocol::WebUIProtocol::new(
330            parser.into_fragment_records(),
331        )));
332        let mut session = StreamingSession::new(
333            Arc::new(WebUIHandler::new()),
334            protocol,
335            SessionOptions::new("index.html", "/"),
336        )
337        .expect("session");
338
339        let state = Value::Object(serde_json::Map::new());
340        let mut step = session.start(&state).expect("start");
341        let mut steps = 1usize;
342        while !step.done {
343            step = match step.boundary.as_ref() {
344                Some(boundary) => session
345                    .resume(boundary.instance_id, &state, BoundaryMode::Final)
346                    .expect("resume"),
347                None => session.advance().expect("advance"),
348            };
349            steps += 1;
350        }
351        // start, commit, advance, commit, advance.
352        assert_eq!(steps, 5);
353        assert!(session.is_done());
354    }
355}