use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::time::Duration;
use liminal::protocol::{Frame, FrameType, ProtocolVersion, decode};
use crate::SdkError;
use super::super::core::{
DriverOutput, FrameCorrelation, FrameViolation, ResponseExpectation, SocketCommand,
SocketEvent, SocketFailure, TransportTerminal, WebSocketFrameDriver,
};
use super::super::encode_frame;
use super::super::std_socket::{IO_TIMEOUT, SocketRead, WsSocket};
const CLIENT_MIN_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
const CLIENT_MAX_VERSION: ProtocolVersion = ProtocolVersion::new(1, 0);
const CONVERSATION_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
pub(super) const APPLICATION_STREAM_ID: u32 = 1;
pub(super) struct WsLink {
pub(super) socket: WsSocket,
pub(super) driver: WebSocketFrameDriver,
pub(super) open_conversations: BTreeSet<u64>,
}
pub(super) enum ExchangeError {
Loss(Box<LinkLoss>),
Local(SdkError),
}
pub(super) struct LinkLoss {
pub(super) terminal: TransportTerminal,
pub(super) description: String,
}
impl ExchangeError {
fn loss(terminal: TransportTerminal, description: String) -> Self {
Self::Loss(Box::new(LinkLoss {
terminal,
description,
}))
}
}
pub(super) fn handshake(link: &mut WsLink, auth_token: &[u8]) -> Result<(), ExchangeError> {
let connect = Frame::Connect {
flags: 0,
min_version: CLIENT_MIN_VERSION,
max_version: CLIENT_MAX_VERSION,
auth_token: auth_token.to_vec(),
};
let bytes = encode_frame(&connect).map_err(ExchangeError::Local)?;
match exchange_correlated(link, bytes)? {
Frame::ConnectAck { .. } => Ok(()),
Frame::ConnectError {
reason_code,
message,
..
} => Err(ExchangeError::Local(SdkError::Connection {
description: format!(
"server rejected connection (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
})),
other => Err(ExchangeError::Local(unexpected_response(
"ConnectAck",
&other,
))),
}
}
pub(super) fn send_frame(link: &mut WsLink, frame: &Frame) -> Result<(), ExchangeError> {
let bytes = encode_frame(frame).map_err(ExchangeError::Local)?;
dispatch_send(link, bytes, ResponseExpectation::None)
}
pub(super) fn exchange_correlated(
link: &mut WsLink,
bytes: Vec<u8>,
) -> Result<Frame, ExchangeError> {
dispatch_send(link, bytes, ResponseExpectation::Correlated)?;
receive_correlated(link)
}
fn dispatch_send(
link: &mut WsLink,
bytes: Vec<u8>,
expectation: ResponseExpectation,
) -> Result<(), ExchangeError> {
let command = link
.driver
.command_send(bytes, expectation)
.map_err(|refusal| {
ExchangeError::Local(SdkError::Protocol {
description: format!("websocket driver refused the send: {refusal:?}"),
})
})?;
let SocketCommand::SendBinary(payload) = command else {
return Err(ExchangeError::Local(SdkError::Protocol {
description: "websocket driver emitted a non-send command for a send".to_string(),
}));
};
if let Err(failure) = link.socket.send_binary(payload) {
let step = link.driver.handle_event(SocketEvent::Failed(failure));
if step.command == Some(SocketCommand::Close) {
link.socket.execute_close();
}
let detail = link
.socket
.last_failure_detail()
.unwrap_or("websocket send failed")
.to_string();
return Err(ExchangeError::loss(
TransportTerminal::SocketFailed(failure),
detail,
));
}
Ok(())
}
fn receive_correlated(link: &mut WsLink) -> Result<Frame, ExchangeError> {
loop {
match read_step(link, None)? {
LinkRead::Frame { bytes, correlation } => match correlation {
FrameCorrelation::CorrelatedResponse => return decode_response(link, &bytes),
FrameCorrelation::UnsolicitedDelivery => {}
FrameCorrelation::UnsolicitedFrame => {
return Err(ExchangeError::Local(SdkError::Protocol {
description:
"websocket driver classified a response as unsolicited while an \
exchange was outstanding"
.to_string(),
}));
}
},
LinkRead::TimedOut => {
let step = link
.driver
.handle_event(SocketEvent::Failed(SocketFailure::Transport));
if step.command == Some(SocketCommand::Close) {
link.socket.execute_close();
}
return Err(ExchangeError::loss(
TransportTerminal::SocketFailed(SocketFailure::Transport),
"timed out waiting for the correlated websocket response".to_string(),
));
}
}
}
}
pub(super) fn receive_participant_frame(link: &mut WsLink) -> Result<Frame, ExchangeError> {
loop {
match read_step(link, None)? {
LinkRead::TimedOut => {
return Err(ExchangeError::Local(SdkError::Connection {
description: "timed out waiting for a participant websocket frame".to_string(),
}));
}
LinkRead::Frame { bytes, correlation } => match correlation {
FrameCorrelation::UnsolicitedDelivery => {}
FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
return decode_response(link, &bytes);
}
},
}
}
}
enum LinkRead {
Frame {
bytes: Vec<u8>,
correlation: FrameCorrelation,
},
TimedOut,
}
fn read_step(link: &mut WsLink, window: Option<Duration>) -> Result<LinkRead, ExchangeError> {
if let Some(window) = window {
link.socket
.set_read_timeout(Some(window))
.map_err(ExchangeError::Local)?;
}
let read = link.socket.read_event();
if window.is_some() {
link.socket
.set_read_timeout(Some(IO_TIMEOUT))
.map_err(ExchangeError::Local)?;
}
let event = match read {
SocketRead::TimedOut => return Ok(LinkRead::TimedOut),
SocketRead::Event(event) => event,
};
let step = link.driver.handle_event(event);
if step.command == Some(SocketCommand::Close) {
link.socket.execute_close();
}
match step.output {
DriverOutput::Frame { bytes, correlation } => Ok(LinkRead::Frame { bytes, correlation }),
DriverOutput::Terminal(terminal) => {
let detail = link
.socket
.last_failure_detail()
.map_or_else(|| terminal_description(&terminal), ToString::to_string);
Err(ExchangeError::loss(terminal, detail))
}
DriverOutput::Opened | DriverOutput::PostTerminalIgnored(_) | DriverOutput::Refused(_) => {
Err(ExchangeError::Local(SdkError::Protocol {
description: format!(
"websocket driver produced an unexpected read output: {:?}",
step.output
),
}))
}
}
}
pub(super) fn decode_response(link: &mut WsLink, bytes: &[u8]) -> Result<Frame, ExchangeError> {
match decode(bytes) {
Ok((frame, consumed)) if consumed == bytes.len() => Ok(frame),
Ok((_, consumed)) => Err(undecodable_body(
link,
&format!(
"canonical decode consumed {consumed} of {} websocket message bytes",
bytes.len()
),
)),
Err(error) => Err(undecodable_body(
link,
&format!("canonical decode failed: {error}"),
)),
}
}
fn undecodable_body(link: &mut WsLink, detail: &str) -> ExchangeError {
if link.driver.command_close().is_ok() {
link.socket.execute_close();
}
ExchangeError::loss(
TransportTerminal::ProtocolViolation(FrameViolation::UndecodableBody),
format!("websocket message failed canonical decode: {detail}"),
)
}
pub(super) fn ensure_conversation_open(
link: &mut WsLink,
conversation_id: u64,
subject: &str,
) -> Result<(), ExchangeError> {
if link.open_conversations.contains(&conversation_id) {
return Ok(());
}
let open = Frame::ConversationOpen {
flags: 0,
stream_id: APPLICATION_STREAM_ID,
conversation_id,
subject: subject.to_string(),
};
send_frame(link, &open)?;
drain_conversation_error(link, conversation_id)?;
link.open_conversations.insert(conversation_id);
Ok(())
}
pub(super) fn drain_conversation_error(
link: &mut WsLink,
conversation_id: u64,
) -> Result<(), ExchangeError> {
loop {
match read_step(link, Some(CONVERSATION_DRAIN_TIMEOUT))? {
LinkRead::TimedOut => return Ok(()),
LinkRead::Frame { bytes, correlation } => match correlation {
FrameCorrelation::UnsolicitedDelivery => {}
FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
let frame = decode_response(link, &bytes)?;
match frame {
Frame::ConversationError {
conversation_id: replied,
reason_code,
message,
..
} => {
return Err(ExchangeError::Local(SdkError::Conversation {
conversation_id: replied.to_string(),
description: format!(
"server rejected conversation {conversation_id} \
(reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}));
}
other => {
return Err(ExchangeError::Local(unexpected_response(
"ConversationError or no reply",
&other,
)));
}
}
}
},
}
}
}
pub(in crate::remote::websocket) fn unexpected_response(
expected: &str,
actual: &Frame,
) -> SdkError {
SdkError::Protocol {
description: format!(
"expected {expected} frame, received {:?}",
FrameType::from(u8::from(actual.frame_type()))
),
}
}
fn terminal_description(terminal: &TransportTerminal) -> String {
match terminal {
TransportTerminal::PeerClosed => "server closed the websocket connection".to_string(),
TransportTerminal::CloseCompleted => "websocket close completed".to_string(),
TransportTerminal::SocketFailed(failure) => {
format!("websocket socket failed: {failure:?}")
}
TransportTerminal::ProtocolViolation(violation) => {
format!("websocket message violated the frame contract: {violation:?}")
}
}
}
pub(super) fn loss_error(terminal: &TransportTerminal, description: &str) -> SdkError {
match terminal {
TransportTerminal::ProtocolViolation(_)
| TransportTerminal::SocketFailed(
SocketFailure::UnsupportedTextMessage | SocketFailure::MessageBeyondBound,
) => SdkError::Protocol {
description: description.to_string(),
},
TransportTerminal::PeerClosed
| TransportTerminal::CloseCompleted
| TransportTerminal::SocketFailed(SocketFailure::Transport) => SdkError::Connection {
description: description.to_string(),
},
}
}