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
118 use liminal::protocol::{
119 CausalContext, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG,
120 PUBLISH_IDEMPOTENCY_KEY_FLAG, SchemaId,
121 };
122 use liminal_protocol::outcome::ReconnectState;
123 use spin::Mutex;
124
125 use crate::remote::ServerAddress;
126 use crate::remote::participant::ParticipantResponseProvenance;
127 use crate::remote::protocol::{
128 ParticipantRemoteTransport, ParticipantTransportFrame, RemoteTransport,
129 WireConversationRequest, WirePublishRequest, WireResumeRequest, WireSubscribeRequest,
130 };
131 use crate::{DeliveryAck, PressureResponse, SdkError};
132
133 use super::connection::WsConnection;
134 use super::liminal_ws_message_bound;
135
136 const APPLICATION_STREAM_ID: u32 = 1;
138 const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
140 const SCHEMALESS_SCHEMA: &[u8] = &[];
142
143 pub struct WebSocketRemoteTransport {
151 connection: Arc<Mutex<WsConnection>>,
152 }
153
154 impl fmt::Debug for WebSocketRemoteTransport {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 formatter
157 .debug_struct("WebSocketRemoteTransport")
158 .finish_non_exhaustive()
159 }
160 }
161
162 impl WebSocketRemoteTransport {
163 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
173 Self::connect_with_auth(server_address, &[])
174 }
175
176 pub fn connect_with_auth(
188 server_address: &ServerAddress,
189 auth_token: &[u8],
190 ) -> Result<Self, SdkError> {
191 let bound = liminal_ws_message_bound()?;
192 let connection = WsConnection::connect(server_address.as_str(), auth_token, bound)?;
193 Ok(Self {
194 connection: Arc::new(Mutex::new(connection)),
195 })
196 }
197
198 pub fn reconnect(&self) -> Result<(), SdkError> {
209 self.connection.lock().reconnect()
210 }
211
212 #[must_use]
214 pub fn reconnect_state(&self) -> ReconnectState {
215 self.connection.lock().reconnect_state()
216 }
217
218 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
219 let mut connection = self.connection.lock();
220 connection.round_trip(request)
221 }
222 }
223
224 impl ParticipantRemoteTransport for WebSocketRemoteTransport {
225 fn send_participant(
226 &self,
227 _server_address: &ServerAddress,
228 request: &liminal_protocol::wire::ClientRequest,
229 ) -> Result<ParticipantResponseProvenance, SdkError> {
230 self.connection.lock().send_participant(request)
231 }
232
233 fn receive_participant(
234 &self,
235 _server_address: &ServerAddress,
236 ) -> Result<ParticipantTransportFrame, SdkError> {
237 let (frame, provenance) = self.connection.lock().receive_participant()?;
238 Ok(ParticipantTransportFrame { frame, provenance })
239 }
240
241 fn reconnect_participant(
242 &self,
243 _server_address: &ServerAddress,
244 ) -> Result<ParticipantResponseProvenance, SdkError> {
245 self.connection.lock().reconnect_participant()
246 }
247 }
248
249 impl RemoteTransport for WebSocketRemoteTransport {
250 fn publish(
251 &self,
252 _server_address: &ServerAddress,
253 request: &WirePublishRequest,
254 ) -> Result<PressureResponse, SdkError> {
255 let frame = build_publish_frame(request);
256 let response = self.round_trip(&frame)?;
257 publish_response(response)
258 }
259
260 fn publish_with_delivery(
261 &self,
262 _server_address: &ServerAddress,
263 request: &WirePublishRequest,
264 ) -> Result<DeliveryAck, SdkError> {
265 let frame = build_publish_frame(request);
266 let response = self.round_trip(&frame)?;
267 publish_delivery_response(response)
268 }
269
270 fn subscribe(
276 &self,
277 _server_address: &ServerAddress,
278 request: &WireSubscribeRequest,
279 ) -> Result<(), SdkError> {
280 let frame = Frame::Subscribe {
281 flags: 0,
282 stream_id: request.stream_id(),
283 channel: request.channel().to_string(),
284 accepted_schemas: Vec::new(),
287 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
288 };
289 let response = self.round_trip(&frame)?;
290 subscribe_response(response)
291 }
292
293 fn send_conversation(
294 &self,
295 _server_address: &ServerAddress,
296 request: &WireConversationRequest,
297 ) -> Result<(), SdkError> {
298 let conversation_label = request.conversation_id().as_str();
299 let conversation_id = conversation_wire_id(conversation_label);
300 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
301 let mut connection = self.connection.lock();
302 connection.send_conversation_message(conversation_id, conversation_label, envelope)
303 }
304
305 fn request_reply_conversation(
306 &self,
307 _server_address: &ServerAddress,
308 request: &WireConversationRequest,
309 ) -> Result<Vec<u8>, SdkError> {
310 let conversation_label = request.conversation_id().as_str();
311 let conversation_id = conversation_wire_id(conversation_label);
312 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
313 let mut connection = self.connection.lock();
314 connection.conversation_request_reply(conversation_id, conversation_label, envelope)
315 }
316
317 fn resume(
318 &self,
319 _server_address: &ServerAddress,
320 request: &WireResumeRequest,
321 ) -> Result<(), SdkError> {
322 let _ = (request.subscription_id(), request.resume_from_sequence());
327 Err(SdkError::Protocol {
328 description:
329 "resume is not yet supported over the WebSocket transport; re-subscribe to \
330 trigger server replay"
331 .to_string(),
332 })
333 }
334 }
335
336 fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
337 MessageEnvelope::new(
338 schema_id_from_bytes(schema_bytes),
339 CausalContext::independent(),
340 payload.to_vec(),
341 )
342 }
343
344 fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
348 let mut id = [0_u8; SchemaId::WIRE_LEN];
349 let mut hash = fnv1a(schema_bytes).to_be_bytes();
350 for (index, slot) in id.iter_mut().enumerate() {
351 *slot = hash[index % hash.len()];
352 if index % hash.len() == hash.len() - 1 {
353 hash = fnv1a(&hash).to_be_bytes();
354 }
355 }
356 SchemaId::new(id)
357 }
358
359 fn conversation_wire_id(conversation_id: &str) -> u64 {
360 fnv1a(conversation_id.as_bytes())
361 }
362
363 fn fnv1a(bytes: &[u8]) -> u64 {
365 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
366 const PRIME: u64 = 0x0000_0100_0000_01b3;
367 let mut hash = OFFSET_BASIS;
368 for byte in bytes {
369 hash ^= u64::from(*byte);
370 hash = hash.wrapping_mul(PRIME);
371 }
372 hash
373 }
374
375 fn build_publish_frame(request: &WirePublishRequest) -> Frame {
379 let envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
380 let flags = match request.idempotency_key() {
381 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
382 None => 0,
383 };
384 Frame::Publish {
385 flags,
386 stream_id: APPLICATION_STREAM_ID,
387 channel: request.channel().to_string(),
388 envelope,
389 idempotency_key: request.idempotency_key().map(ToString::to_string),
390 }
391 }
392
393 fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
394 match frame {
395 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
396 Frame::PublishError {
397 reason_code,
398 message,
399 ..
400 } => Err(SdkError::Backpressure {
401 reason: format!(
402 "server rejected publish (reason {reason_code}): {}",
403 message.unwrap_or_else(|| "no detail".to_string())
404 ),
405 }),
406 other => Err(super::connection::unexpected_response("PublishAck", &other)),
407 }
408 }
409
410 fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
413 match frame {
414 Frame::PublishAck { flags, .. } => {
415 let accepted = flags & PUBLISH_DELIVERED_FLAG != 0;
416 Ok(DeliveryAck::new(PressureResponse::Accept, accepted))
417 }
418 Frame::PublishError {
419 reason_code,
420 message,
421 ..
422 } => Err(SdkError::Backpressure {
423 reason: format!(
424 "server rejected publish (reason {reason_code}): {}",
425 message.unwrap_or_else(|| "no detail".to_string())
426 ),
427 }),
428 other => Err(super::connection::unexpected_response("PublishAck", &other)),
429 }
430 }
431
432 fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
433 match frame {
434 Frame::SubscribeAck { .. } => Ok(()),
435 Frame::SubscribeError {
436 reason_code,
437 message,
438 ..
439 } => Err(SdkError::Protocol {
440 description: format!(
441 "server rejected subscribe (reason {reason_code}): {}",
442 message.unwrap_or_else(|| "no detail".to_string())
443 ),
444 }),
445 other => Err(super::connection::unexpected_response(
446 "SubscribeAck",
447 &other,
448 )),
449 }
450 }
451}
452
453#[cfg(feature = "std")]
454pub use transport::WebSocketRemoteTransport;