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_IDEMPOTENCY_KEY_FLAG, ProtocolVersion,
121 SchemaId,
122 };
123 use liminal_protocol::outcome::ReconnectState;
124 use spin::Mutex;
125
126 use crate::remote::ServerAddress;
127 use crate::remote::participant::{ParticipantPumpDiagnostics, ParticipantResponseProvenance};
128 use crate::remote::protocol::{
129 ParticipantPumpDiagnosticsSource, ParticipantRemoteTransport, ParticipantTransportFrame,
130 RemoteTransport, WireConversationRequest, WirePublishRequest, WireResumeRequest,
131 WireSubscribeRequest,
132 };
133 use crate::remote::publish_reply;
134 use crate::{DeliveryAck, PressureResponse, SdkError};
135
136 use super::connection::WsConnection;
137 use super::liminal_ws_message_bound;
138
139 const APPLICATION_STREAM_ID: u32 = 1;
141 const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
143 const SCHEMALESS_SCHEMA: &[u8] = &[];
145
146 pub struct WebSocketRemoteTransport {
154 connection: Arc<Mutex<WsConnection>>,
155 }
156
157 impl fmt::Debug for WebSocketRemoteTransport {
158 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159 formatter
160 .debug_struct("WebSocketRemoteTransport")
161 .finish_non_exhaustive()
162 }
163 }
164
165 impl WebSocketRemoteTransport {
166 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
176 Self::connect_with_auth(server_address, &[])
177 }
178
179 pub fn connect_with_auth(
191 server_address: &ServerAddress,
192 auth_token: &[u8],
193 ) -> Result<Self, SdkError> {
194 let bound = liminal_ws_message_bound()?;
195 let connection = WsConnection::connect(server_address.as_str(), auth_token, bound)?;
196 Ok(Self {
197 connection: Arc::new(Mutex::new(connection)),
198 })
199 }
200
201 pub fn reconnect(&self) -> Result<(), SdkError> {
212 self.connection.lock().reconnect()
213 }
214
215 #[must_use]
217 pub fn reconnect_state(&self) -> ReconnectState {
218 self.connection.lock().reconnect_state()
219 }
220
221 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
222 let mut connection = self.connection.lock();
223 connection.round_trip(request)
224 }
225 }
226
227 impl ParticipantRemoteTransport for WebSocketRemoteTransport {
228 fn send_participant(
229 &self,
230 _server_address: &ServerAddress,
231 request: &liminal_protocol::wire::ClientRequest,
232 ) -> Result<ParticipantResponseProvenance, SdkError> {
233 self.connection.lock().send_participant(request)
234 }
235
236 fn receive_participant(
237 &self,
238 _server_address: &ServerAddress,
239 ) -> Result<ParticipantTransportFrame, SdkError> {
240 let (frame, provenance) = self.connection.lock().receive_participant()?;
241 Ok(ParticipantTransportFrame { frame, provenance })
242 }
243
244 fn receive_participant_within(
245 &self,
246 _server_address: &ServerAddress,
247 budget: Duration,
248 ) -> Result<Option<ParticipantTransportFrame>, SdkError> {
249 let Some((frame, provenance)) =
250 self.connection.lock().receive_participant_within(budget)?
251 else {
252 return Ok(None);
253 };
254 Ok(Some(ParticipantTransportFrame { frame, provenance }))
255 }
256
257 fn try_receive_participant(&self) -> Result<Option<ParticipantTransportFrame>, SdkError> {
258 let frame = self.connection.lock().try_receive_participant()?;
259 Ok(frame.map(|(frame, provenance)| ParticipantTransportFrame { frame, provenance }))
260 }
261
262 fn reconnect_participant(
263 &self,
264 _server_address: &ServerAddress,
265 ) -> Result<ParticipantResponseProvenance, SdkError> {
266 self.connection.lock().reconnect_participant()
267 }
268 }
269
270 impl ParticipantPumpDiagnosticsSource for WebSocketRemoteTransport {
271 fn participant_pump_diagnostics(&self) -> ParticipantPumpDiagnostics {
272 self.connection.lock().pump_diagnostics()
273 }
274 }
275
276 impl RemoteTransport for WebSocketRemoteTransport {
277 fn publish(
278 &self,
279 _server_address: &ServerAddress,
280 request: &WirePublishRequest,
281 ) -> Result<PressureResponse, SdkError> {
282 let frame = build_publish_frame(request);
283 let response = self.round_trip(&frame)?;
284 publish_response(response)
285 }
286
287 fn publish_with_delivery(
288 &self,
289 _server_address: &ServerAddress,
290 request: &WirePublishRequest,
291 ) -> Result<DeliveryAck, SdkError> {
292 let frame = build_publish_frame(request);
293 let response = self.round_trip(&frame)?;
294 publish_delivery_response(response)
295 }
296
297 fn subscribe(
303 &self,
304 _server_address: &ServerAddress,
305 request: &WireSubscribeRequest,
306 ) -> Result<(), SdkError> {
307 let frame = Frame::Subscribe {
308 flags: 0,
309 stream_id: request.stream_id(),
310 channel: request.channel().to_string(),
311 accepted_schemas: Vec::new(),
314 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
315 };
316 let response = self.round_trip(&frame)?;
317 subscribe_response(response)
318 }
319
320 fn send_conversation(
321 &self,
322 _server_address: &ServerAddress,
323 request: &WireConversationRequest,
324 ) -> Result<(), SdkError> {
325 let conversation_label = request.conversation_id().as_str();
326 let conversation_id = conversation_wire_id(conversation_label);
327 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
328 let mut connection = self.connection.lock();
329 connection.send_conversation_message(conversation_id, conversation_label, envelope)
330 }
331
332 fn request_reply_conversation(
333 &self,
334 _server_address: &ServerAddress,
335 request: &WireConversationRequest,
336 ) -> Result<Vec<u8>, SdkError> {
337 let conversation_label = request.conversation_id().as_str();
338 let conversation_id = conversation_wire_id(conversation_label);
339 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
340 let mut connection = self.connection.lock();
341 connection.conversation_request_reply(conversation_id, conversation_label, envelope)
342 }
343
344 fn resume(
345 &self,
346 _server_address: &ServerAddress,
347 request: &WireResumeRequest,
348 ) -> Result<(), SdkError> {
349 let _ = (request.subscription_id(), request.resume_from_sequence());
354 Err(SdkError::Protocol {
355 description:
356 "resume is not yet supported over the WebSocket transport; re-subscribe for live \
357 delivery only; messages published while disconnected are not replayed"
358 .to_string(),
359 })
360 }
361
362 fn negotiated_version(&self) -> Option<ProtocolVersion> {
363 self.connection.lock().negotiated_version()
364 }
365 }
366
367 fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
368 MessageEnvelope::new(
369 schema_id_from_bytes(schema_bytes),
370 CausalContext::independent(),
371 payload.to_vec(),
372 )
373 }
374
375 fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
379 SchemaId::from_schema_bytes(schema_bytes)
380 }
381
382 fn conversation_wire_id(conversation_id: &str) -> u64 {
383 fnv1a(conversation_id.as_bytes())
384 }
385
386 fn fnv1a(bytes: &[u8]) -> u64 {
388 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
389 const PRIME: u64 = 0x0000_0100_0000_01b3;
390 let mut hash = OFFSET_BASIS;
391 for byte in bytes {
392 hash ^= u64::from(*byte);
393 hash = hash.wrapping_mul(PRIME);
394 }
395 hash
396 }
397
398 fn build_publish_frame(request: &WirePublishRequest) -> Frame {
402 let mut envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
403 envelope.causal_context = request.causal_context();
404 let flags = match request.idempotency_key() {
405 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
406 None => 0,
407 };
408 Frame::Publish {
409 flags,
410 stream_id: APPLICATION_STREAM_ID,
411 channel: request.channel().to_string(),
412 envelope,
413 idempotency_key: request.idempotency_key().map(ToString::to_string),
414 }
415 }
416
417 fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
418 match frame {
419 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
420 Frame::PublishError {
421 reason_code,
422 message,
423 ..
424 } => Err(publish_reply::publish_error(reason_code, message)),
425 other => Err(super::connection::unexpected_response("PublishAck", &other)),
426 }
427 }
428
429 fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
433 match frame {
434 Frame::PublishAck {
435 flags,
436 durable_position,
437 ..
438 } => Ok(publish_reply::delivery_ack(flags, durable_position)),
439 Frame::PublishError {
440 reason_code,
441 message,
442 ..
443 } => Err(publish_reply::publish_error(reason_code, message)),
444 other => Err(super::connection::unexpected_response("PublishAck", &other)),
445 }
446 }
447
448 fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
449 match frame {
450 Frame::SubscribeAck { .. } => Ok(()),
451 Frame::SubscribeError {
452 reason_code,
453 message,
454 ..
455 } => Err(SdkError::Protocol {
456 description: format!(
457 "server rejected subscribe (reason {reason_code}): {}",
458 message.unwrap_or_else(|| "no detail".to_string())
459 ),
460 }),
461 other => Err(super::connection::unexpected_response(
462 "SubscribeAck",
463 &other,
464 )),
465 }
466 }
467
468 #[cfg(test)]
469 mod tests {
470 use super::*;
471
472 #[test]
473 fn publish_unresolved_maps_to_publish_unresolved() {
474 let frame = Frame::PublishError {
475 flags: 0,
476 stream_id: 1,
477 reason_code: 0x0103,
478 message: Some("orders k1".into()),
479 };
480 assert!(matches!(
481 publish_response(frame.clone()),
482 Err(SdkError::PublishUnresolved { description }) if description == "orders k1"
483 ));
484 assert!(matches!(
485 publish_delivery_response(frame),
486 Err(SdkError::PublishUnresolved { description }) if description == "orders k1"
487 ));
488 }
489 }
490}
491
492#[cfg(feature = "std")]
493pub use transport::WebSocketRemoteTransport;