Skip to main content

webui_handler/streaming/
owned.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Host-owned streaming sessions for bindings that cannot hold a Rust borrow.
5//!
6//! [`StreamingResponse`](super::StreamingResponse) borrows its handler,
7//! protocol, options, and transport for the life of one response. That is the
8//! right shape for a Rust host, which keeps the response on its own stack, but
9//! it cannot cross a foreign-function boundary: a Node class instance, a C
10//! opaque pointer, or a .NET `SafeHandle` must stay alive between calls that
11//! Rust never sees.
12//!
13//! [`StreamingSession`] closes that gap without self-referential storage. It
14//! retains the handler and protocol behind `Arc`, parks the response's owned
15//! progress between calls, and rebuilds the borrowed half for the duration of
16//! each call. Nothing borrowed ever spans a call boundary, so this is ordinary
17//! safe Rust.
18//!
19//! It also inverts transport ownership. Each method returns the bytes it
20//! produced instead of writing them, so the host writes to its own socket and
21//! applies its own backpressure — the thing a push-based chunk callback cannot
22//! express.
23
24use std::sync::Arc;
25
26use serde_json::Value;
27
28use super::session::{BoundaryId, BoundaryMode, ParkedResponse};
29use super::StreamingResponse;
30use crate::route_handler::Protocol;
31use crate::{FlushWriter, RenderOptions, ResponseWriter, Result, WebUIHandler};
32
33/// Collects response bytes for a host that owns its own transport.
34///
35/// `flush` is deliberately a no-op: a semantic flush means "these bytes are
36/// complete and may be sent", and for an encoder that is exactly the point at
37/// which the method returns them to the host.
38#[derive(Default)]
39pub struct BufferSink {
40    bytes: Vec<u8>,
41}
42
43impl ResponseWriter for BufferSink {
44    fn write(&mut self, content: &str) -> Result<()> {
45        self.bytes.extend_from_slice(content.as_bytes());
46        Ok(())
47    }
48
49    fn end(&mut self) -> Result<()> {
50        Ok(())
51    }
52}
53
54impl FlushWriter for BufferSink {
55    fn flush(&mut self) -> Result<()> {
56        Ok(())
57    }
58}
59
60/// Owned per-response configuration, so a session outlives its caller's strings.
61#[derive(Clone, Debug)]
62pub struct SessionOptions {
63    /// Entry fragment to render.
64    pub entry_id: String,
65    /// Request path used for route matching.
66    pub request_path: String,
67    /// Optional CSP nonce for generated inline `<script>` tags.
68    pub nonce: Option<String>,
69    /// Optional HTML injected at the structural `head_end` boundary.
70    pub head_inject: Option<String>,
71    /// Optional HTML injected at the structural `body_end` boundary.
72    pub body_inject: Option<String>,
73}
74
75impl SessionOptions {
76    /// Create options for an entry fragment and request path.
77    #[must_use]
78    pub fn new(entry_id: impl Into<String>, request_path: impl Into<String>) -> Self {
79        Self {
80            entry_id: entry_id.into(),
81            request_path: request_path.into(),
82            nonce: None,
83            head_inject: None,
84            body_inject: None,
85        }
86    }
87}
88
89/// A progressive HTML response a foreign host drives one call at a time.
90///
91/// Every method returns the bytes that call produced. The host writes them to
92/// its transport and decides when to continue, so backpressure and interleaved
93/// async work stay under host control.
94///
95/// Ordering rules match [`StreamingResponse`]: the shell first, then each
96/// boundary exactly once in declaration order, updates only after the target
97/// boundary commits, and `finish` last. Violations return an actionable error
98/// rather than corrupting the stream, and any render or transport failure
99/// permanently poisons the session because bytes may already have been sent.
100pub struct StreamingSession {
101    handler: Arc<WebUIHandler>,
102    protocol: Arc<Protocol>,
103    options: SessionOptions,
104    /// `None` only while a call is in flight, or once `finish` consumed the
105    /// response either successfully or fatally.
106    parked: Option<ParkedResponse>,
107    sink: BufferSink,
108    boundary_count: usize,
109    /// Set only when a terminal record actually reached the transport.
110    finished: bool,
111}
112
113impl StreamingSession {
114    /// Start a host-driven progressive response.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error when the entry is missing or its compiled streaming
119    /// structure is malformed.
120    pub fn new(
121        handler: Arc<WebUIHandler>,
122        protocol: Arc<Protocol>,
123        options: SessionOptions,
124    ) -> Result<Self> {
125        let mut sink = BufferSink::default();
126        let (parked, boundary_count) = {
127            let render_options = render_options(&options);
128            let response = handler.stream_response(&protocol, &render_options, &mut sink)?;
129            let boundary_count = response.boundary_count();
130            (response.park(), boundary_count)
131        };
132        Ok(Self {
133            handler,
134            protocol,
135            options,
136            parked: Some(parked),
137            sink,
138            boundary_count,
139            finished: false,
140        })
141    }
142
143    /// Resolve a free-form authored boundary name to an integer handle.
144    ///
145    /// Resolve once and reuse the handle; hot calls never hash a name.
146    ///
147    /// # Errors
148    ///
149    /// Returns an actionable error listing valid names when the entry does not
150    /// declare `name`.
151    pub fn boundary(&self, name: &str) -> Result<BoundaryId> {
152        let names = self
153            .protocol
154            .streaming_boundary_names(&self.options.entry_id);
155        names
156            .iter()
157            .position(|candidate| candidate == name)
158            .ok_or_else(|| super::error::unknown_boundary_name_error(name, names))
159            .and_then(BoundaryId::from_index)
160    }
161
162    /// Number of compile-time boundaries in this entry.
163    #[must_use]
164    pub fn boundary_count(&self) -> usize {
165        self.boundary_count
166    }
167
168    /// Whether the response has emitted its terminal record.
169    ///
170    /// Stays `false` after a rejected or failed `finish`, because no terminal
171    /// record reached the transport in either case.
172    #[must_use]
173    pub fn is_finished(&self) -> bool {
174        self.finished
175    }
176
177    /// Render the document prefix before the first boundary.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error when called out of order or when rendering fails.
182    pub fn write_shell(&mut self, state: &Value) -> Result<Vec<u8>> {
183        self.run(|response| response.write_shell(state))
184    }
185
186    /// Render and commit the next compile-time boundary.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error when boundaries are written out of declaration order or
191    /// when rendering fails.
192    pub fn write_boundary(
193        &mut self,
194        boundary: BoundaryId,
195        state: &Value,
196        mode: BoundaryMode,
197    ) -> Result<Vec<u8>> {
198        self.run(|response| response.write_boundary(boundary, state, mode))
199    }
200
201    /// Push a projected state patch to an already committed updatable boundary.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error when the boundary has not committed, was committed as
206    /// final, or when `state` is not a JSON object.
207    pub fn update(&mut self, boundary: BoundaryId, state: &Value) -> Result<Vec<u8>> {
208        self.run(|response| response.update(boundary, state))
209    }
210
211    /// Render the document tail and emit the terminal record.
212    ///
213    /// The session is finished afterwards and every later call fails.
214    ///
215    /// # Errors
216    ///
217    /// Returns an error when boundaries remain uncommitted or rendering fails.
218    pub fn finish(&mut self, state: &Value) -> Result<Vec<u8>> {
219        let parked = self.take_parked("finish")?;
220        let result = {
221            let Self {
222                handler,
223                protocol,
224                options,
225                sink,
226                parked: slot,
227                ..
228            } = self;
229            let render_options = render_options(options);
230            match StreamingResponse::unpark(parked, handler, protocol, &render_options, sink) {
231                // Ordering violations are rejected before any byte is written,
232                // so park the response again and let the host commit what is
233                // still outstanding instead of losing the open response.
234                Ok(response) => match response.ensure_finishable() {
235                    Ok(()) => response.finish(state),
236                    Err(error) => {
237                        *slot = Some(response.park());
238                        Err(error)
239                    }
240                },
241                Err(error) => Err(error),
242            }
243        };
244        match result {
245            Ok(()) => {
246                self.finished = true;
247                Ok(std::mem::take(&mut self.sink.bytes))
248            }
249            Err(error) => {
250                self.sink.bytes = Vec::new();
251                Err(error)
252            }
253        }
254    }
255
256    fn take_parked(&mut self, operation: &str) -> Result<ParkedResponse> {
257        if let Some(parked) = self.parked.take() {
258            return Ok(parked);
259        }
260        let reason = if self.finished {
261            "the streaming response has already finished"
262        } else {
263            "the streaming response failed while emitting its terminal record and \
264             cannot continue; bytes may already have been sent"
265        };
266        Err(super::error::boundary_order_error(operation, reason))
267    }
268
269    /// Rebuild the borrowed response, run one operation, and park it again.
270    fn run(
271        &mut self,
272        operation: impl FnOnce(&mut StreamingResponse<'_, BufferSink>) -> Result<()>,
273    ) -> Result<Vec<u8>> {
274        let parked = self.take_parked("streaming operation")?;
275        let Self {
276            handler,
277            protocol,
278            options,
279            sink,
280            parked: slot,
281            ..
282        } = self;
283        let render_options = render_options(options);
284        let mut response =
285            StreamingResponse::unpark(parked, handler, protocol, &render_options, sink)?;
286        let result = operation(&mut response);
287        *slot = Some(response.park());
288        result?;
289        Ok(std::mem::take(&mut self.sink.bytes))
290    }
291}
292
293fn render_options(options: &SessionOptions) -> RenderOptions<'_> {
294    RenderOptions {
295        entry_id: &options.entry_id,
296        request_path: &options.request_path,
297        nonce: options.nonce.as_deref(),
298        head_inject: options.head_inject.as_deref(),
299        body_inject: options.body_inject.as_deref(),
300    }
301}