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}