#[path = "stream.rs"]
mod stream;
use alloc::format;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use core::time::Duration;
use liminal::protocol::{
CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG, PUBLISH_IDEMPOTENCY_KEY_FLAG,
SchemaId,
};
use liminal_server::server::embedded::EmbeddedServer;
use spin::Mutex;
use crate::{DeliveryAck, PressureResponse, SdkError};
use self::stream::LoopbackStream;
use super::ServerAddress;
use super::framing::{Connection, unexpected_frame};
use super::participant::ParticipantResponseProvenance;
use super::protocol::{
ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
};
const APPLICATION_STREAM_ID: u32 = 1;
const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
const SCHEMALESS_SCHEMA: &[u8] = &[];
struct ConnectionSlot {
connection: Connection<LoopbackStream>,
provenance: ParticipantResponseProvenance,
next_attempt_id: u64,
next_connection_id: u64,
}
pub(super) struct LoopbackRemoteTransport {
server: Arc<EmbeddedServer>,
connection: Arc<Mutex<ConnectionSlot>>,
auth_token: Vec<u8>,
}
impl fmt::Debug for LoopbackRemoteTransport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LoopbackRemoteTransport")
.finish_non_exhaustive()
}
}
impl LoopbackRemoteTransport {
pub(super) fn connect_with_auth(
server: Arc<EmbeddedServer>,
auth_token: &[u8],
) -> Result<Self, SdkError> {
let connection = open_connection(&server, auth_token)?;
Ok(Self {
server,
connection: Arc::new(Mutex::new(ConnectionSlot {
connection,
provenance: ParticipantResponseProvenance::new(1, 1),
next_attempt_id: 2,
next_connection_id: 2,
})),
auth_token: auth_token.to_vec(),
})
}
fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
self.connection.lock().connection.round_trip(request)
}
}
fn open_connection(
server: &EmbeddedServer,
auth_token: &[u8],
) -> Result<Connection<LoopbackStream>, SdkError> {
let end = server
.connect_loopback()
.map_err(|source| SdkError::Connection {
description: format!("failed to open an in-process connection: {source}"),
})?;
Connection::established(LoopbackStream::new(end), auth_token)
}
impl ParticipantRemoteTransport for LoopbackRemoteTransport {
fn send_participant(
&self,
_server_address: &ServerAddress,
request: &liminal_protocol::wire::ClientRequest,
) -> Result<ParticipantResponseProvenance, SdkError> {
let mut slot = self.connection.lock();
slot.connection.send_participant(request)?;
Ok(slot.provenance)
}
fn receive_participant(
&self,
_server_address: &ServerAddress,
) -> Result<ParticipantTransportFrame, SdkError> {
let mut slot = self.connection.lock();
let frame = slot.connection.receive_participant()?;
Ok(ParticipantTransportFrame {
frame,
provenance: slot.provenance,
})
}
fn receive_participant_within(
&self,
_server_address: &ServerAddress,
budget: Duration,
) -> Result<Option<ParticipantTransportFrame>, SdkError> {
let mut slot = self.connection.lock();
let Some(frame) = slot.connection.receive_participant_within(budget)? else {
return Ok(None);
};
Ok(Some(ParticipantTransportFrame {
frame,
provenance: slot.provenance,
}))
}
fn reconnect_participant(
&self,
_server_address: &ServerAddress,
) -> Result<ParticipantResponseProvenance, SdkError> {
let mut slot = self.connection.lock();
let attempt_id = slot.next_attempt_id;
slot.next_attempt_id =
slot.next_attempt_id
.checked_add(1)
.ok_or_else(|| SdkError::Connection {
description: "participant transport attempt identity exhausted".to_string(),
})?;
let connection = open_connection(&self.server, &self.auth_token)?;
let connection_id = slot.next_connection_id;
slot.next_connection_id =
slot.next_connection_id
.checked_add(1)
.ok_or_else(|| SdkError::Connection {
description: "participant transport connection identity exhausted".to_string(),
})?;
let provenance = ParticipantResponseProvenance::new(connection_id, attempt_id);
slot.connection = connection;
slot.provenance = provenance;
Ok(provenance)
}
}
impl RemoteTransport for LoopbackRemoteTransport {
fn publish(
&self,
_server_address: &ServerAddress,
request: &WirePublishRequest,
) -> Result<PressureResponse, SdkError> {
let frame = build_publish_frame(request);
publish_response(self.round_trip(&frame)?)
}
fn publish_with_delivery(
&self,
_server_address: &ServerAddress,
request: &WirePublishRequest,
) -> Result<DeliveryAck, SdkError> {
let frame = build_publish_frame(request);
publish_delivery_response(self.round_trip(&frame)?)
}
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,
};
subscribe_response(self.round_trip(&frame)?)
}
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());
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());
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 in-process 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(unexpected_frame("PublishAck", &other)),
}
}
fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
match frame {
Frame::PublishAck { flags, .. } => Ok(DeliveryAck::new(
PressureResponse::Accept,
flags & PUBLISH_DELIVERED_FLAG != 0,
)),
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(unexpected_frame("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(unexpected_frame("SubscribeAck", &other)),
}
}