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#[cfg(feature = "std")]
37mod subscription_signals;
38
39pub use binding::{
40 AttemptFateOutcome, AttemptFateRefusal, DetachLossOutcome, LossRecordOutcome,
41 LossRecordRefusal, OpenRequestDecision, OpenRequestRefusal, WebSocketAuthorityBinding,
42};
43pub use core::{
44 CommandRefusal, DriverOutput, DriverPhase, DriverStep, EventRefusal, FrameCorrelation,
45 FrameViolation, PostTerminalEvent, ResponseExpectation, SocketCommand, SocketEvent,
46 SocketFailure, TransportTerminal, WebSocketFrameDriver,
47};
48#[cfg(feature = "std")]
49pub use subscription::{WebSocketDeliveredMessage, WebSocketSubscriptionStream};
50
51use alloc::format;
52
53use liminal_protocol::wire::FRAME_MAX;
54
55use crate::SdkError;
56
57pub fn liminal_ws_message_bound() -> Result<usize, SdkError> {
74 usize::try_from(FRAME_MAX).map_err(|_| SdkError::Protocol {
75 description: format!(
76 "websocket transport cannot start: this target's usize cannot represent the \
77 liminal frame bound of {FRAME_MAX} bytes"
78 ),
79 })
80}
81
82#[cfg(feature = "std")]
84pub(crate) fn connection_error(description: &str) -> SdkError {
85 use alloc::string::ToString;
86 SdkError::Connection {
87 description: description.to_string(),
88 }
89}
90
91#[cfg(feature = "std")]
93fn encode_frame(frame: &liminal::protocol::Frame) -> Result<alloc::vec::Vec<u8>, SdkError> {
94 use liminal::protocol::{encode, encoded_len};
95 let len = encoded_len(frame).map_err(|error| SdkError::Protocol {
96 description: format!("wire codec error: {error}"),
97 })?;
98 let mut bytes = alloc::vec![0_u8; len];
99 let written = encode(frame, &mut bytes).map_err(|error| SdkError::Protocol {
100 description: format!("wire codec error: {error}"),
101 })?;
102 if written != bytes.len() {
103 return Err(SdkError::Protocol {
104 description: "wire encoder reported an invalid byte count".to_string(),
105 });
106 }
107 Ok(bytes)
108}
109
110#[cfg(feature = "std")]
111mod transport {
112 use alloc::format;
115 use alloc::string::ToString;
116 use alloc::sync::Arc;
117 use alloc::vec::Vec;
118 use core::fmt;
119 use core::time::Duration;
120
121 use liminal::protocol::{
122 CausalContext, Frame, MessageEnvelope, PUBLISH_IDEMPOTENCY_KEY_FLAG, ProtocolVersion,
123 SchemaId,
124 };
125 use liminal_protocol::outcome::ReconnectState;
126 use spin::Mutex;
127
128 use crate::remote::ServerAddress;
129 use crate::remote::participant::{ParticipantPumpDiagnostics, ParticipantResponseProvenance};
130 use crate::remote::protocol::{
131 ParticipantPumpDiagnosticsSource, ParticipantRemoteTransport, ParticipantTransportFrame,
132 RemoteTransport, WireConversationRequest, WirePublishRequest, WireResumeRequest,
133 WireSubscribeRequest,
134 };
135 use crate::remote::publish_reply;
136 use crate::{DeliveryAck, PressureResponse, SdkError};
137
138 use super::connection::WsConnection;
139 use super::liminal_ws_message_bound;
140
141 const APPLICATION_STREAM_ID: u32 = 1;
143 const DEFAULT_MAX_IN_FLIGHT: u32 = 1;
145 const SCHEMALESS_SCHEMA: &[u8] = &[];
147
148 pub struct WebSocketRemoteTransport {
156 connection: Arc<Mutex<WsConnection>>,
157 }
158
159 impl fmt::Debug for WebSocketRemoteTransport {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 formatter
162 .debug_struct("WebSocketRemoteTransport")
163 .finish_non_exhaustive()
164 }
165 }
166
167 impl WebSocketRemoteTransport {
168 pub fn connect(server_address: &ServerAddress) -> Result<Self, SdkError> {
178 Self::connect_with_auth(server_address, &[])
179 }
180
181 pub fn connect_with_auth(
193 server_address: &ServerAddress,
194 auth_token: &[u8],
195 ) -> Result<Self, SdkError> {
196 let bound = liminal_ws_message_bound()?;
197 let connection = WsConnection::connect(server_address.as_str(), auth_token, bound)?;
198 Ok(Self {
199 connection: Arc::new(Mutex::new(connection)),
200 })
201 }
202
203 pub fn reconnect(&self) -> Result<(), SdkError> {
214 self.connection.lock().reconnect()
215 }
216
217 #[must_use]
219 pub fn reconnect_state(&self) -> ReconnectState {
220 self.connection.lock().reconnect_state()
221 }
222
223 fn round_trip(&self, request: &Frame) -> Result<Frame, SdkError> {
224 let mut connection = self.connection.lock();
225 connection.round_trip(request)
226 }
227 }
228
229 impl ParticipantRemoteTransport for WebSocketRemoteTransport {
230 fn send_participant(
231 &self,
232 _server_address: &ServerAddress,
233 request: &liminal_protocol::wire::ClientRequest,
234 ) -> Result<ParticipantResponseProvenance, SdkError> {
235 self.connection.lock().send_participant(request)
236 }
237
238 fn receive_participant(
239 &self,
240 _server_address: &ServerAddress,
241 ) -> Result<ParticipantTransportFrame, SdkError> {
242 let (frame, provenance) = self.connection.lock().receive_participant()?;
243 Ok(ParticipantTransportFrame { frame, provenance })
244 }
245
246 fn receive_participant_within(
247 &self,
248 _server_address: &ServerAddress,
249 budget: Duration,
250 ) -> Result<Option<ParticipantTransportFrame>, SdkError> {
251 let Some((frame, provenance)) =
252 self.connection.lock().receive_participant_within(budget)?
253 else {
254 return Ok(None);
255 };
256 Ok(Some(ParticipantTransportFrame { frame, provenance }))
257 }
258
259 fn try_receive_participant(&self) -> Result<Option<ParticipantTransportFrame>, SdkError> {
260 let frame = self.connection.lock().try_receive_participant()?;
261 Ok(frame.map(|(frame, provenance)| ParticipantTransportFrame { frame, provenance }))
262 }
263
264 fn reconnect_participant(
265 &self,
266 _server_address: &ServerAddress,
267 ) -> Result<ParticipantResponseProvenance, SdkError> {
268 self.connection.lock().reconnect_participant()
269 }
270 }
271
272 impl ParticipantPumpDiagnosticsSource for WebSocketRemoteTransport {
273 fn participant_pump_diagnostics(&self) -> ParticipantPumpDiagnostics {
274 self.connection.lock().pump_diagnostics()
275 }
276 }
277
278 impl RemoteTransport for WebSocketRemoteTransport {
279 fn publish(
280 &self,
281 _server_address: &ServerAddress,
282 request: &WirePublishRequest,
283 ) -> Result<PressureResponse, SdkError> {
284 let frame = build_publish_frame(request);
285 let response = self.round_trip(&frame)?;
286 publish_response(response)
287 }
288
289 fn publish_with_delivery(
290 &self,
291 _server_address: &ServerAddress,
292 request: &WirePublishRequest,
293 ) -> Result<DeliveryAck, SdkError> {
294 let frame = build_publish_frame(request);
295 let response = self.round_trip(&frame)?;
296 publish_delivery_response(response)
297 }
298
299 fn subscribe(
305 &self,
306 _server_address: &ServerAddress,
307 request: &WireSubscribeRequest,
308 ) -> Result<(), SdkError> {
309 let frame = Frame::Subscribe {
310 flags: 0,
311 stream_id: request.stream_id(),
312 channel: request.channel().to_string(),
313 accepted_schemas: Vec::new(),
316 max_in_flight: DEFAULT_MAX_IN_FLIGHT,
317 };
318 let response = self.round_trip(&frame)?;
319 subscribe_response(response)
320 }
321
322 fn send_conversation(
323 &self,
324 _server_address: &ServerAddress,
325 request: &WireConversationRequest,
326 ) -> Result<(), SdkError> {
327 let conversation_label = request.conversation_id().as_str();
328 let conversation_id = conversation_wire_id(conversation_label);
329 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
330 let mut connection = self.connection.lock();
331 connection.send_conversation_message(conversation_id, conversation_label, envelope)
332 }
333
334 fn request_reply_conversation(
335 &self,
336 _server_address: &ServerAddress,
337 request: &WireConversationRequest,
338 ) -> Result<Vec<u8>, SdkError> {
339 let conversation_label = request.conversation_id().as_str();
340 let conversation_id = conversation_wire_id(conversation_label);
341 let envelope = build_envelope(SCHEMALESS_SCHEMA, request.payload());
342 let mut connection = self.connection.lock();
343 connection.conversation_request_reply(conversation_id, conversation_label, envelope)
344 }
345
346 fn resume(
347 &self,
348 _server_address: &ServerAddress,
349 request: &WireResumeRequest,
350 ) -> Result<(), SdkError> {
351 let _ = (request.subscription_id(), request.resume_from_sequence());
356 Err(SdkError::Protocol {
357 description:
358 "resume is not yet supported over the WebSocket transport; re-subscribe for live \
359 delivery only; messages published while disconnected are not replayed"
360 .to_string(),
361 })
362 }
363
364 fn negotiated_version(&self) -> Option<ProtocolVersion> {
365 self.connection.lock().negotiated_version()
366 }
367 }
368
369 fn build_envelope(schema_bytes: &[u8], payload: &[u8]) -> MessageEnvelope {
370 MessageEnvelope::new(
371 schema_id_from_bytes(schema_bytes),
372 CausalContext::independent(),
373 payload.to_vec(),
374 )
375 }
376
377 fn schema_id_from_bytes(schema_bytes: &[u8]) -> SchemaId {
381 SchemaId::from_schema_bytes(schema_bytes)
382 }
383
384 fn conversation_wire_id(conversation_id: &str) -> u64 {
385 fnv1a(conversation_id.as_bytes())
386 }
387
388 fn fnv1a(bytes: &[u8]) -> u64 {
390 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
391 const PRIME: u64 = 0x0000_0100_0000_01b3;
392 let mut hash = OFFSET_BASIS;
393 for byte in bytes {
394 hash ^= u64::from(*byte);
395 hash = hash.wrapping_mul(PRIME);
396 }
397 hash
398 }
399
400 fn build_publish_frame(request: &WirePublishRequest) -> Frame {
404 let mut envelope = build_envelope(request.schema().schema.as_ref(), request.payload());
405 envelope.causal_context = request.causal_context();
406 let flags = match request.idempotency_key() {
407 Some(_) => PUBLISH_IDEMPOTENCY_KEY_FLAG,
408 None => 0,
409 };
410 Frame::Publish {
411 flags,
412 stream_id: APPLICATION_STREAM_ID,
413 channel: request.channel().to_string(),
414 envelope,
415 idempotency_key: request.idempotency_key().map(ToString::to_string),
416 }
417 }
418
419 fn publish_response(frame: Frame) -> Result<PressureResponse, SdkError> {
420 match frame {
421 Frame::PublishAck { .. } => Ok(PressureResponse::Accept),
422 Frame::PublishError {
423 reason_code,
424 message,
425 ..
426 } => Err(publish_reply::publish_error(reason_code, message)),
427 other => Err(super::connection::unexpected_response("PublishAck", &other)),
428 }
429 }
430
431 fn publish_delivery_response(frame: Frame) -> Result<DeliveryAck, SdkError> {
435 match frame {
436 Frame::PublishAck {
437 flags,
438 durable_position,
439 ..
440 } => Ok(publish_reply::delivery_ack(flags, durable_position)),
441 Frame::PublishError {
442 reason_code,
443 message,
444 ..
445 } => Err(publish_reply::publish_error(reason_code, message)),
446 other => Err(super::connection::unexpected_response("PublishAck", &other)),
447 }
448 }
449
450 fn subscribe_response(frame: Frame) -> Result<(), SdkError> {
451 match frame {
452 Frame::SubscribeAck { .. } => Ok(()),
453 Frame::SubscribeError {
454 reason_code,
455 message,
456 ..
457 } => Err(SdkError::Protocol {
458 description: format!(
459 "server rejected subscribe (reason {reason_code}): {}",
460 message.unwrap_or_else(|| "no detail".to_string())
461 ),
462 }),
463 other => Err(super::connection::unexpected_response(
464 "SubscribeAck",
465 &other,
466 )),
467 }
468 }
469
470 #[cfg(test)]
471 mod tests {
472 use super::*;
473
474 #[test]
475 fn publish_unresolved_maps_to_publish_unresolved() {
476 let frame = Frame::PublishError {
477 flags: 0,
478 stream_id: 1,
479 reason_code: 0x0103,
480 message: Some("orders k1".into()),
481 };
482 assert!(matches!(
483 publish_response(frame.clone()),
484 Err(SdkError::PublishUnresolved { description }) if description == "orders k1"
485 ));
486 assert!(matches!(
487 publish_delivery_response(frame),
488 Err(SdkError::PublishUnresolved { description }) if description == "orders k1"
489 ));
490 }
491 }
492}
493
494#[cfg(feature = "std")]
495pub use transport::WebSocketRemoteTransport;