pub mod binding;
pub mod core;
pub mod web_socket;
#[cfg(feature = "std")]
mod connection;
#[cfg(feature = "std")]
mod participant;
#[cfg(feature = "std")]
mod std_socket;
#[cfg(feature = "std")]
mod subscription;
pub use binding::{
AttemptFateOutcome, AttemptFateRefusal, DetachLossOutcome, LossRecordOutcome,
LossRecordRefusal, OpenRequestDecision, OpenRequestRefusal, WebSocketAuthorityBinding,
};
pub use core::{
CommandRefusal, DriverOutput, DriverPhase, DriverStep, EventRefusal, FrameCorrelation,
FrameViolation, PostTerminalEvent, ResponseExpectation, SocketCommand, SocketEvent,
SocketFailure, TransportTerminal, WebSocketFrameDriver,
};
#[cfg(feature = "std")]
pub use subscription::{WebSocketDeliveredMessage, WebSocketSubscriptionStream};
use alloc::format;
use liminal_protocol::wire::FRAME_MAX;
use crate::SdkError;
pub fn liminal_ws_message_bound() -> Result<usize, SdkError> {
usize::try_from(FRAME_MAX).map_err(|_| SdkError::Protocol {
description: format!(
"websocket transport cannot start: this target's usize cannot represent the \
liminal frame bound of {FRAME_MAX} bytes"
),
})
}
#[cfg(feature = "std")]
pub(crate) fn connection_error(description: &str) -> SdkError {
use alloc::string::ToString;
SdkError::Connection {
description: description.to_string(),
}
}
#[cfg(feature = "std")]
fn encode_frame(frame: &liminal::protocol::Frame) -> Result<alloc::vec::Vec<u8>, SdkError> {
use liminal::protocol::{encode, encoded_len};
let len = encoded_len(frame).map_err(|error| SdkError::Protocol {
description: format!("wire codec error: {error}"),
})?;
let mut bytes = alloc::vec![0_u8; len];
let written = encode(frame, &mut bytes).map_err(|error| SdkError::Protocol {
description: format!("wire codec error: {error}"),
})?;
if written != bytes.len() {
return Err(SdkError::Protocol {
description: "wire encoder reported an invalid byte count".to_string(),
});
}
Ok(bytes)
}
#[cfg(feature = "std")]
mod transport {
use alloc::format;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use liminal::protocol::{
CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG,
PUBLISH_IDEMPOTENCY_KEY_FLAG, SchemaId,
};
use liminal_protocol::outcome::ReconnectState;
use spin::Mutex;
use crate::remote::ServerAddress;
use crate::remote::participant::ParticipantResponseProvenance;
use crate::remote::protocol::{
ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
};
use crate::{DeliveryAck, PressureResponse, SdkError};
use super::connection::WsConnection;
use super::liminal_ws_message_bound;
const APPLICATION_STREAM_ID: u32 = 1;
const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
const SCHEMALESS_SCHEMA: &[u8] = &[];
pub struct WebSocketRemoteTransport {
connection: Arc<Mutex<WsConnection>>,
}
impl fmt::Debug for WebSocketRemoteTransport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebSocketRemoteTransport")
.finish_non_exhaustive()
}
}
impl WebSocketRemoteTransport {
pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
Self::connect_with_auth(server_address, &[])
}
pub fn connect_with_auth(
server_address: &ServerAddress,
auth_token: &[u8],
) -> Result<Self, SdkError> {
let bound = liminal_ws_message_bound()?;
let connection = WsConnection::connect(server_address.as_str(), auth_token, bound)?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
})
}
pub fn reconnect(&self) -> Result<(), SdkError> {
self.connection.lock().reconnect()
}
#[must_use]
pub fn reconnect_state(&self) -> ReconnectState {
self.connection.lock().reconnect_state()
}
fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
let mut connection = self.connection.lock();
connection.round_trip(request)
}
}
impl ParticipantRemoteTransport for WebSocketRemoteTransport {
fn send_participant(
&self,
_server_address: &ServerAddress,
request: &liminal_protocol::wire::ClientRequest,
) -> Result<ParticipantResponseProvenance, SdkError> {
self.connection.lock().send_participant(request)
}
fn receive_participant(
&self,
_server_address: &ServerAddress,
) -> Result<ParticipantTransportFrame, SdkError> {
let (frame, provenance) = self.connection.lock().receive_participant()?;
Ok(ParticipantTransportFrame { frame, provenance })
}
fn reconnect_participant(
&self,
_server_address: &ServerAddress,
) -> Result<ParticipantResponseProvenance, SdkError> {
self.connection.lock().reconnect_participant()
}
}
impl RemoteTransport for WebSocketRemoteTransport {
fn publish(
&self,
_server_address: &ServerAddress,
request: &WirePublishRequest,
) -> Result<PressureResponse, SdkError> {
let frame = build_publish_frame(request);
let response = self.round_trip(&frame)?;
publish_response(response)
}
fn publish_with_delivery(
&self,
_server_address: &ServerAddress,
request: &WirePublishRequest,
) -> Result<DeliveryAck, SdkError> {
let frame = build_publish_frame(request);
let response = self.round_trip(&frame)?;
publish_delivery_response(response)
}
fn subscribe(
&self,
_server_address: &ServerAddress,
request: &WireSubscribeRequest,
) -> Result<(), SdkError> {
let frame = Frame::Subscribe {
flags: 0,
stream_id: request.stream_id(),
channel: request.channel().to_string(),
accepted_schemas: Vec::new(),
max_in_flight: DEFAULT_MAX_IN_FLIGHT,
};
let response = self.round_trip(&frame)?;
subscribe_response(response)
}
fn send_conversation(
&self,
_server_address: &ServerAddress,
request: &WireConversationRequest,
) -> Result<(), SdkError> {
let conversation_label = request.conversation_id().as_str();
let conversation_id = conversation_wire_id(conversation_label);
let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
let mut connection = self.connection.lock();
connection.send_conversation_message(conversation_id, conversation_label, envelope)
}
fn request_reply_conversation(
&self,
_server_address: &ServerAddress,
request: &WireConversationRequest,
) -> Result<Vec<u8>, SdkError> {
let conversation_label = request.conversation_id().as_str();
let conversation_id = conversation_wire_id(conversation_label);
let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
let mut connection = self.connection.lock();
connection.conversation_request_reply(conversation_id, conversation_label, envelope)
}
fn resume(
&self,
_server_address: &ServerAddress,
request: &WireResumeRequest,
) -> Result<(), SdkError> {
let _ = (request.subscription_id(), request.resume_from_sequence());
Err(SdkError::Protocol {
description:
"resume is not yet supported over the WebSocket transport; re-subscribe to \
trigger server replay"
.to_string(),
})
}
}
fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
MessageEnvelope::new(
schema_id_from_bytes(schema_bytes),
CausalContext::independent(),
payload.to_vec(),
)
}
fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
let mut id = [0_u8; SchemaId::WIRE_LEN];
let mut hash = fnv1a(schema_bytes).to_be_bytes();
for (index, slot) in id.iter_mut().enumerate() {
*slot = hash[index % hash.len()];
if index % hash.len() == hash.len() - 1 {
hash = fnv1a(&hash).to_be_bytes();
}
}
SchemaId::new(id)
}
fn conversation_wire_id(conversation_id: &str) -> u64 {
fnv1a(conversation_id.as_bytes())
}
fn fnv1a(bytes: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET_BASIS;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(PRIME);
}
hash
}
fn build_publish_frame(request: &WirePublishRequest) -> Frame {
let envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
let flags = match request.idempotency_key() {
Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
None => 0,
};
Frame::Publish {
flags,
stream_id: APPLICATION_STREAM_ID,
channel: request.channel().to_string(),
envelope,
idempotency_key: request.idempotency_key().map(ToString::to_string),
}
}
fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
match frame {
Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
Frame::PublishError {
reason_code,
message,
..
} => Err(SdkError::Backpressure {
reason: format!(
"server rejected publish (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}),
other => Err(super::connection::unexpected_response("PublishAck", &other)),
}
}
fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
match frame {
Frame::PublishAck { flags, .. } => {
let accepted = flags & PUBLISH_DELIVERED_FLAG != 0;
Ok(DeliveryAck::new(PressureResponse::Accept, accepted))
}
Frame::PublishError {
reason_code,
message,
..
} => Err(SdkError::Backpressure {
reason: format!(
"server rejected publish (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}),
other => Err(super::connection::unexpected_response("PublishAck", &other)),
}
}
fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
match frame {
Frame::SubscribeAck { .. } => Ok(()),
Frame::SubscribeError {
reason_code,
message,
..
} => Err(SdkError::Protocol {
description: format!(
"server rejected subscribe (reason {reason_code}): {}",
message.unwrap_or_else(|| "no detail".to_string())
),
}),
other => Err(super::connection::unexpected_response(
"SubscribeAck",
&other,
)),
}
}
}
#[cfg(feature = "std")]
pub use transport::WebSocketRemoteTransport;