1pub mod binding;
25pub mod core;
26pub mod web_socket;
27
28#[cfg(feature = "std")]
29mod connection;
30#[cfg(feature = "std")]
31mod participant;
32#[cfg(feature = "std")]
33mod std_socket;
34#[cfg(feature = "std")]
35mod subscription;
36
37pub use binding::{
38 AttemptFateOutcome, AttemptFateRefusal, DetachLossOutcome, LossRecordOutcome,
39 LossRecordRefusal, OpenRequestDecision, OpenRequestRefusal, WebSocketAuthorityBinding,
40};
41pub use core::{
42 CommandRefusal, DriverOutput, DriverPhase, DriverStep, EventRefusal, FrameCorrelation,
43 FrameViolation, PostTerminalEvent, ResponseExpectation, SocketCommand, SocketEvent,
44 SocketFailure, TransportTerminal, WebSocketFrameDriver,
45};
46#[cfg(feature = "std")]
47pub use subscription::{WebSocketDeliveredMessage, WebSocketSubscriptionStream};
48
49use alloc::format;
50
51use liminal_protocol::wire::FRAME_MAX;
52
53use crate::SdkError;
54
55pub fn liminal_ws_message_bound() -> Result<usize, SdkError> {
72 usize::try_from(FRAME_MAX).map_err(|_| SdkError::Protocol {
73 description: format!(
74 "websocket transport cannot start: this target's usize cannot represent the \
75 liminal frame bound of {FRAME_MAX} bytes"
76 ),
77 })
78}
79
80#[cfg(feature = "std")]
82pub(crate) fn connection_error(description: &str) -> SdkError {
83 use alloc::string::ToString;
84 SdkError::Connection {
85 description: description.to_string(),
86 }
87}
88
89#[cfg(feature = "std")]
91fn encode_frame(frame: &liminal::protocol::Frame) -> Result<alloc::vec::Vec<u8>, SdkError> {
92 use liminal::protocol::{encode, encoded_len};
93 let len = encoded_len(frame).map_err(|error| SdkError::Protocol {
94 description: format!("wire codec error: {error}"),
95 })?;
96 let mut bytes = alloc::vec![0_u8; len];
97 let written = encode(frame, &mut bytes).map_err(|error| SdkError::Protocol {
98 description: format!("wire codec error: {error}"),
99 })?;
100 if written != bytes.len() {
101 return Err(SdkError::Protocol {
102 description: "wire encoder reported an invalid byte count".to_string(),
103 });
104 }
105 Ok(bytes)
106}
107
108#[cfg(feature = "std")]
109mod transport {
110 use alloc::format;
113 use alloc::string::ToString;
114 use alloc::sync::Arc;
115 use alloc::vec::Vec;
116 use core::fmt;
117 use core::time::Duration;
118
119 use liminal::protocol::{
120 CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG,
121 PUBLISH_IDEMPOTENCY_KEY_FLAG, SchemaId,
122 };
123 use liminal_protocol::outcome::ReconnectState;
124 use spin::Mutex;
125
126 use crate::remote::ServerAddress;
127 use crate::remote::participant::ParticipantResponseProvenance;
128 use crate::remote::protocol::{
129 ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
130 WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
131 };
132 use crate::{DeliveryAck, PressureResponse, SdkError};
133
134 use super::connection::WsConnection;
135 use super::liminal_ws_message_bound;
136
137 const APPLICATION_STREAM_ID: u32 = 1;
139 const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
141 const SCHEMALESS_SCHEMA: &[u8] = &[];
143
144 pub struct WebSocketRemoteTransport {
152 connection: Arc<Mutex<WsConnection>>,
153 }
154
155 impl fmt::Debug for WebSocketRemoteTransport {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 formatter
158 .debug_struct("WebSocketRemoteTransport")
159 .finish_non_exhaustive()
160 }
161 }
162
163 impl WebSocketRemoteTransport {
164 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
174 Self::connect_with_auth(server_address, &[])
175 }
176
177 pub fn connect_with_auth(
189 server_address: &ServerAddress,
190 auth_token: &[u8],
191 ) -> Result<Self, SdkError> {
192 let bound = liminal_ws_message_bound()?;
193 let connection = WsConnection::connect(server_address.as_str(), auth_token, bound)?;
194 Ok(Self {
195 connection: Arc::new(Mutex::new(connection)),
196 })
197 }
198
199 pub fn reconnect(&self) -> Result<(), SdkError> {
210 self.connection.lock().reconnect()
211 }
212
213 #[must_use]
215 pub fn reconnect_state(&self) -> ReconnectState {
216 self.connection.lock().reconnect_state()
217 }
218
219 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
220 let mut connection = self.connection.lock();
221 connection.round_trip(request)
222 }
223 }
224
225 impl ParticipantRemoteTransport for WebSocketRemoteTransport {
226 fn send_participant(
227 &self,
228 _server_address: &ServerAddress,
229 request: &liminal_protocol::wire::ClientRequest,
230 ) -> Result<ParticipantResponseProvenance, SdkError> {
231 self.connection.lock().send_participant(request)
232 }
233
234 fn receive_participant(
235 &self,
236 _server_address: &ServerAddress,
237 ) -> Result<ParticipantTransportFrame, SdkError> {
238 let (frame, provenance) = self.connection.lock().receive_participant()?;
239 Ok(ParticipantTransportFrame { frame, provenance })
240 }
241
242 fn receive_participant_within(
243 &self,
244 _server_address: &ServerAddress,
245 budget: Duration,
246 ) -> Result<Option<ParticipantTransportFrame>, SdkError> {
247 let Some((frame, provenance)) =
248 self.connection.lock().receive_participant_within(budget)?
249 else {
250 return Ok(None);
251 };
252 Ok(Some(ParticipantTransportFrame { frame, provenance }))
253 }
254
255 fn reconnect_participant(
256 &self,
257 _server_address: &ServerAddress,
258 ) -> Result<ParticipantResponseProvenance, SdkError> {
259 self.connection.lock().reconnect_participant()
260 }
261 }
262
263 impl RemoteTransport for WebSocketRemoteTransport {
264 fn publish(
265 &self,
266 _server_address: &ServerAddress,
267 request: &WirePublishRequest,
268 ) -> Result<PressureResponse, SdkError> {
269 let frame = build_publish_frame(request);
270 let response = self.round_trip(&frame)?;
271 publish_response(response)
272 }
273
274 fn publish_with_delivery(
275 &self,
276 _server_address: &ServerAddress,
277 request: &WirePublishRequest,
278 ) -> Result<DeliveryAck, SdkError> {
279 let frame = build_publish_frame(request);
280 let response = self.round_trip(&frame)?;
281 publish_delivery_response(response)
282 }
283
284 fn subscribe(
290 &self,
291 _server_address: &ServerAddress,
292 request: &WireSubscribeRequest,
293 ) -> Result<(), SdkError> {
294 let frame = Frame::Subscribe {
295 flags: 0,
296 stream_id: request.stream_id(),
297 channel: request.channel().to_string(),
298 accepted_schemas: Vec::new(),
301 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
302 };
303 let response = self.round_trip(&frame)?;
304 subscribe_response(response)
305 }
306
307 fn send_conversation(
308 &self,
309 _server_address: &ServerAddress,
310 request: &WireConversationRequest,
311 ) -> Result<(), SdkError> {
312 let conversation_label = request.conversation_id().as_str();
313 let conversation_id = conversation_wire_id(conversation_label);
314 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
315 let mut connection = self.connection.lock();
316 connection.send_conversation_message(conversation_id, conversation_label, envelope)
317 }
318
319 fn request_reply_conversation(
320 &self,
321 _server_address: &ServerAddress,
322 request: &WireConversationRequest,
323 ) -> Result<Vec<u8>, SdkError> {
324 let conversation_label = request.conversation_id().as_str();
325 let conversation_id = conversation_wire_id(conversation_label);
326 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
327 let mut connection = self.connection.lock();
328 connection.conversation_request_reply(conversation_id, conversation_label, envelope)
329 }
330
331 fn resume(
332 &self,
333 _server_address: &ServerAddress,
334 request: &WireResumeRequest,
335 ) -> Result<(), SdkError> {
336 let _ = (request.subscription_id(), request.resume_from_sequence());
341 Err(SdkError::Protocol {
342 description:
343 "resume is not yet supported over the WebSocket transport; re-subscribe to \
344 trigger server replay"
345 .to_string(),
346 })
347 }
348 }
349
350 fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
351 MessageEnvelope::new(
352 schema_id_from_bytes(schema_bytes),
353 CausalContext::independent(),
354 payload.to_vec(),
355 )
356 }
357
358 fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
362 let mut id = [0_u8; SchemaId::WIRE_LEN];
363 let mut hash = fnv1a(schema_bytes).to_be_bytes();
364 for (index, slot) in id.iter_mut().enumerate() {
365 *slot = hash[index % hash.len()];
366 if index % hash.len() == hash.len() - 1 {
367 hash = fnv1a(&hash).to_be_bytes();
368 }
369 }
370 SchemaId::new(id)
371 }
372
373 fn conversation_wire_id(conversation_id: &str) -> u64 {
374 fnv1a(conversation_id.as_bytes())
375 }
376
377 fn fnv1a(bytes: &[u8]) -> u64 {
379 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
380 const PRIME: u64 = 0x0000_0100_0000_01b3;
381 let mut hash = OFFSET_BASIS;
382 for byte in bytes {
383 hash ^= u64::from(*byte);
384 hash = hash.wrapping_mul(PRIME);
385 }
386 hash
387 }
388
389 fn build_publish_frame(request: &WirePublishRequest) -> Frame {
393 let envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
394 let flags = match request.idempotency_key() {
395 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
396 None => 0,
397 };
398 Frame::Publish {
399 flags,
400 stream_id: APPLICATION_STREAM_ID,
401 channel: request.channel().to_string(),
402 envelope,
403 idempotency_key: request.idempotency_key().map(ToString::to_string),
404 }
405 }
406
407 fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
408 match frame {
409 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
410 Frame::PublishError {
411 reason_code,
412 message,
413 ..
414 } => Err(SdkError::Backpressure {
415 reason: format!(
416 "server rejected publish (reason {reason_code}): {}",
417 message.unwrap_or_else(|| "no detail".to_string())
418 ),
419 }),
420 other => Err(super::connection::unexpected_response("PublishAck", &other)),
421 }
422 }
423
424 fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
427 match frame {
428 Frame::PublishAck { flags, .. } => {
429 let accepted = flags & PUBLISH_DELIVERED_FLAG != 0;
430 Ok(DeliveryAck::new(PressureResponse::Accept, accepted))
431 }
432 Frame::PublishError {
433 reason_code,
434 message,
435 ..
436 } => Err(SdkError::Backpressure {
437 reason: format!(
438 "server rejected publish (reason {reason_code}): {}",
439 message.unwrap_or_else(|| "no detail".to_string())
440 ),
441 }),
442 other => Err(super::connection::unexpected_response("PublishAck", &other)),
443 }
444 }
445
446 fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
447 match frame {
448 Frame::SubscribeAck { .. } => Ok(()),
449 Frame::SubscribeError {
450 reason_code,
451 message,
452 ..
453 } => Err(SdkError::Protocol {
454 description: format!(
455 "server rejected subscribe (reason {reason_code}): {}",
456 message.unwrap_or_else(|| "no detail".to_string())
457 ),
458 }),
459 other => Err(super::connection::unexpected_response(
460 "SubscribeAck",
461 &other,
462 )),
463 }
464 }
465}
466
467#[cfg(feature = "std")]
468pub use transport::WebSocketRemoteTransport;