use std::sync::Arc;
use serde_json::Value;
use super::session::{
BoundaryDescriptor, BoundaryInstanceId, BoundaryMode, SessionCall, SessionCore, StreamStatus,
StreamingState,
};
use super::StreamingSink;
use crate::route_handler::Protocol;
use crate::{FlushWriter, RenderOptions, ResponseWriter, Result, WebUIHandler};
#[derive(Default)]
pub struct BufferSink {
bytes: Vec<u8>,
last_flush: usize,
}
impl BufferSink {
fn reset(&mut self) {
self.bytes.clear();
self.last_flush = 0;
}
}
impl ResponseWriter for BufferSink {
fn write(&mut self, content: &str) -> Result<()> {
self.bytes.extend_from_slice(content.as_bytes());
Ok(())
}
fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
crate::append_attribute_to_bytes(&mut self.bytes, name, value);
Ok(())
}
fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
crate::append_boolean_attribute_to_bytes(&mut self.bytes, name);
Ok(())
}
fn end(&mut self) -> Result<()> {
Ok(())
}
}
impl FlushWriter for BufferSink {
fn flush(&mut self) -> Result<()> {
self.last_flush = self.bytes.len();
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct SessionOptions {
pub entry_id: String,
pub request_path: String,
pub nonce: Option<String>,
pub head_inject: Option<String>,
pub body_inject: Option<String>,
}
impl SessionOptions {
#[must_use]
pub fn new(entry_id: impl Into<String>, request_path: impl Into<String>) -> Self {
Self {
entry_id: entry_id.into(),
request_path: request_path.into(),
nonce: None,
head_inject: None,
body_inject: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StreamStep {
pub bytes: Vec<u8>,
pub boundary: Option<BoundaryDescriptor>,
pub done: bool,
}
pub struct StreamingSession {
handler: Arc<WebUIHandler>,
protocol: Arc<Protocol>,
options: SessionOptions,
core: SessionCore,
sink: BufferSink,
}
impl StreamingSession {
pub fn new(
handler: Arc<WebUIHandler>,
protocol: Arc<Protocol>,
options: SessionOptions,
) -> Result<Self> {
let core = SessionCore::new(&handler, &protocol, &options.entry_id)?;
Ok(Self {
handler,
protocol,
options,
core,
sink: BufferSink::default(),
})
}
pub fn start<'state>(
&mut self,
state: impl Into<StreamingState<'state>>,
) -> Result<StreamStep> {
match state.into() {
StreamingState::Borrowed(state) => self.step(|core, call| core.start(call, state)),
StreamingState::Owned(state) => {
self.step(move |core, call| core.start_owned(call, state))
}
}
}
pub fn resume<'state>(
&mut self,
instance_id: BoundaryInstanceId,
state: impl Into<StreamingState<'state>>,
mode: BoundaryMode,
) -> Result<StreamStep> {
match state.into() {
StreamingState::Borrowed(state) => {
self.step(|core, call| core.resume(call, instance_id, state, mode))
}
StreamingState::Owned(state) => {
self.step(move |core, call| core.resume_owned(call, instance_id, state, mode))
}
}
}
pub fn resume_current(
&mut self,
instance_id: BoundaryInstanceId,
mode: BoundaryMode,
) -> Result<StreamStep> {
self.step(|core, call| core.resume_current(call, instance_id, mode))
}
pub fn advance(&mut self) -> Result<StreamStep> {
self.step(SessionCore::advance)
}
pub fn update(&mut self, instance_id: BoundaryInstanceId, patch: &Value) -> Result<Vec<u8>> {
self.sink.reset();
let options = render_options(&self.options);
let mut sink = StreamingSink {
transport: &mut self.sink,
component_opening: None,
written: 0,
flushed: 0,
};
let result = self.core.update(
SessionCall {
handler: &self.handler,
protocol: &self.protocol,
options: &options,
writer: &mut sink,
},
instance_id,
patch,
);
match result {
Ok(()) => {
verify_flush_boundary(&self.sink)?;
Ok(std::mem::take(&mut self.sink.bytes))
}
Err(error) => {
self.sink.reset();
Err(error)
}
}
}
#[must_use]
pub fn is_done(&self) -> bool {
self.core.done
}
fn step(
&mut self,
operation: impl FnOnce(&mut SessionCore, SessionCall<'_, '_>) -> Result<StreamStatus>,
) -> Result<StreamStep> {
self.sink.reset();
let options = render_options(&self.options);
let mut sink = StreamingSink {
transport: &mut self.sink,
component_opening: None,
written: 0,
flushed: 0,
};
let status = operation(
&mut self.core,
SessionCall {
handler: &self.handler,
protocol: &self.protocol,
options: &options,
writer: &mut sink,
},
);
match status {
Ok(status) => {
verify_flush_boundary(&self.sink)?;
Ok(StreamStep {
bytes: std::mem::take(&mut self.sink.bytes),
boundary: status.boundary,
done: status.done,
})
}
Err(error) => {
self.sink.reset();
Err(error)
}
}
}
}
fn verify_flush_boundary(sink: &BufferSink) -> Result<()> {
if sink.last_flush == sink.bytes.len() {
return Ok(());
}
Err(unflushed_step_error())
}
#[cold]
#[inline(never)]
fn unflushed_step_error() -> crate::HandlerError {
crate::HandlerError::Invariant(
"a streaming step buffered bytes past its last flush boundary".to_string(),
)
}
fn render_options(options: &SessionOptions) -> RenderOptions<'_> {
RenderOptions {
entry_id: &options.entry_id,
request_path: &options.request_path,
nonce: options.nonce.as_deref(),
head_inject: options.head_inject.as_deref(),
body_inject: options.body_inject.as_deref(),
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use serde_json::Value;
use super::{BufferSink, SessionOptions, StreamingSession};
use crate::route_handler::Protocol;
use crate::{BoundaryMode, FlushWriter, ResponseWriter, WebUIHandler};
#[test]
fn owned_streaming_session_supports_synchronized_hosts() {
fn assert_send<T: Send>() {}
fn assert_send_sync<T: Send + Sync>() {}
assert_send::<StreamingSession>();
assert_send_sync::<Mutex<StreamingSession>>();
assert_send_sync::<Arc<WebUIHandler>>();
}
#[test]
fn buffer_sink_tracks_the_transport_flush_boundary() {
let mut sink = BufferSink::default();
sink.write("checkpoint").expect("write");
sink.flush().expect("flush");
assert!(super::verify_flush_boundary(&sink).is_ok());
sink.write("tail").expect("write");
assert!(super::verify_flush_boundary(&sink).is_err());
sink.reset();
assert!(super::verify_flush_boundary(&sink).is_ok());
}
#[test]
fn every_owned_step_ends_on_a_flush_boundary() {
let mut parser = webui_parser::HtmlParser::new();
parser
.parse(
"index.html",
concat!(
"<html><head></head><body>",
r#"<boundary name="first"><p>1</p></boundary>"#,
"<hr>",
r#"<boundary name="second"><p>2</p></boundary>"#,
"<footer>tail</footer></body></html>",
),
)
.expect("parse");
let protocol = Arc::new(Protocol::new(webui_protocol::WebUIProtocol::new(
parser.into_fragment_records(),
)));
let mut session = StreamingSession::new(
Arc::new(WebUIHandler::new()),
protocol,
SessionOptions::new("index.html", "/"),
)
.expect("session");
let state = Arc::new(Value::Object(serde_json::Map::new()));
let mut step = session.start(&state).expect("start");
let mut steps = 1usize;
while !step.done {
step = match step.boundary.as_ref() {
Some(boundary) => session
.resume(boundary.instance_id, &state, BoundaryMode::Final)
.expect("resume"),
None => session.advance().expect("advance"),
};
steps += 1;
}
assert_eq!(steps, 5);
assert!(session.is_done());
}
}