1use alloc::format;
14use alloc::string::ToString;
15use alloc::sync::Arc;
16use alloc::vec::Vec;
17
18use core::time::Duration;
19
20use std::net::TcpStream;
21use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
22use std::thread::JoinHandle;
23
24use liminal::protocol::{Frame, SchemaId, decode};
25use liminal_protocol::outcome::ReconnectState;
26use spin::Mutex;
27
28use crate::SdkError;
29use crate::remote::SETUP_TIMEOUT;
30
31use super::binding::{AttemptFateOutcome, OpenRequestDecision, WebSocketAuthorityBinding};
32use super::connection_error;
33use super::core::{
34 DriverOutput, FrameCorrelation, ResponseExpectation, SocketCommand, SocketEvent,
35 WebSocketFrameDriver,
36};
37use super::liminal_ws_message_bound;
38use super::std_socket::{SocketRead, WsSocket};
39
40const CLIENT_MIN_VERSION: liminal::protocol::ProtocolVersion =
42 liminal::protocol::ProtocolVersion::new(1, 0);
43const CLIENT_MAX_VERSION: liminal::protocol::ProtocolVersion =
45 liminal::protocol::ProtocolVersion::new(1, 0);
46const SUBSCRIPTION_STREAM_ID: u32 = 1;
48const SUBSCRIBE_MAX_IN_FLIGHT: u32 = 1024;
50
51#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct WebSocketDeliveredMessage {
58 delivery_seq: u64,
59 schema_id: SchemaId,
60 payload: Vec<u8>,
61}
62
63impl WebSocketDeliveredMessage {
64 #[must_use]
66 pub const fn delivery_seq(&self) -> u64 {
67 self.delivery_seq
68 }
69
70 #[must_use]
72 pub const fn schema_id(&self) -> SchemaId {
73 self.schema_id
74 }
75
76 #[must_use]
78 pub fn payload(&self) -> &[u8] {
79 &self.payload
80 }
81
82 #[must_use]
84 pub fn into_payload(self) -> Vec<u8> {
85 self.payload
86 }
87}
88
89#[derive(Debug)]
92pub struct WebSocketSubscriptionStream {
93 shutdown: TcpStream,
95 subscription_id: u64,
96 inbound: Receiver<Result<WebSocketDeliveredMessage, SdkError>>,
102 binding: Arc<Mutex<WebSocketAuthorityBinding>>,
103 reader: Option<JoinHandle<()>>,
104}
105
106impl WebSocketSubscriptionStream {
107 pub fn open(
119 address: &str,
120 channel: &str,
121 accepted_schemas: Vec<SchemaId>,
122 ) -> Result<Self, SdkError> {
123 Self::open_with_auth(address, channel, accepted_schemas, &[])
124 }
125
126 pub fn open_with_auth(
147 address: &str,
148 channel: &str,
149 accepted_schemas: Vec<SchemaId>,
150 auth_token: &[u8],
151 ) -> Result<Self, SdkError> {
152 let message_bound = liminal_ws_message_bound()?;
153 let mut binding = WebSocketAuthorityBinding::new();
154 match binding.request_open() {
155 OpenRequestDecision::Authorized { .. } => {}
156 OpenRequestDecision::Refused(refusal) => {
157 return Err(connection_error(&format!(
158 "client authority refused the subscription open: {refusal:?}"
159 )));
160 }
161 }
162 match Self::open_link(
163 address,
164 channel,
165 accepted_schemas,
166 message_bound,
167 auth_token,
168 ) {
169 Ok((socket, driver, subscription_id, pending)) => {
170 match binding.connection_established() {
171 AttemptFateOutcome::Recorded { .. } => {}
172 AttemptFateOutcome::Refused(refusal) => {
173 return Err(SdkError::Protocol {
174 description: format!(
175 "client authority refused the Connected fate for the \
176 subscription open: {refusal:?}"
177 ),
178 });
179 }
180 }
181 Self::start(socket, driver, binding, subscription_id, pending)
182 }
183 Err(error) => match binding.open_failed() {
184 AttemptFateOutcome::Recorded { .. } => Err(error),
185 AttemptFateOutcome::Refused(refusal) => Err(SdkError::Protocol {
186 description: format!(
187 "subscription open failed ({error}) and the client authority \
188 refused the Failed fate: {refusal:?}"
189 ),
190 }),
191 },
192 }
193 }
194
195 pub fn recv_timeout(&self, timeout: Duration) -> Result<WebSocketDeliveredMessage, SdkError> {
202 match self.inbound.recv_timeout(timeout) {
203 Ok(delivery) => delivery,
204 Err(error) => {
205 let detail = match error {
206 RecvTimeoutError::Timeout => "no delivery arrived within the timeout",
207 RecvTimeoutError::Disconnected => {
208 "the subscription reader stopped before a delivery arrived"
209 }
210 };
211 Err(connection_error(&format!(
212 "websocket subscription receive failed: {detail}"
213 )))
214 }
215 }
216 }
217
218 #[must_use]
220 pub const fn subscription_id(&self) -> u64 {
221 self.subscription_id
222 }
223
224 #[must_use]
226 pub fn reconnect_state(&self) -> ReconnectState {
227 self.binding.lock().reconnect_state()
228 }
229
230 fn open_link(
233 address: &str,
234 channel: &str,
235 accepted_schemas: Vec<SchemaId>,
236 message_bound: usize,
237 auth_token: &[u8],
238 ) -> Result<
239 (
240 WsSocket,
241 WebSocketFrameDriver,
242 u64,
243 Vec<WebSocketDeliveredMessage>,
244 ),
245 SdkError,
246 > {
247 let mut driver = WebSocketFrameDriver::new();
248 let command = driver
249 .command_open()
250 .map_err(|refusal| SdkError::Protocol {
251 description: format!("subscription driver refused its first open: {refusal:?}"),
252 })?;
253 if command != SocketCommand::Open {
254 return Err(SdkError::Protocol {
255 description: "subscription driver emitted a non-open first command".to_string(),
256 });
257 }
258 let mut socket = WsSocket::connect(address, message_bound)?;
259 socket.set_read_timeout(Some(SETUP_TIMEOUT))?;
264 let step = driver.handle_event(SocketEvent::Opened);
265 if step.output != DriverOutput::Opened {
266 return Err(SdkError::Protocol {
267 description: format!("subscription driver refused the opened socket: {step:?}"),
268 });
269 }
270
271 let mut pending = Vec::new();
272 let connect = Frame::Connect {
273 flags: 0,
274 min_version: CLIENT_MIN_VERSION,
275 max_version: CLIENT_MAX_VERSION,
276 auth_token: auth_token.to_vec(),
277 };
278 match setup_exchange(&mut socket, &mut driver, &connect, &mut pending)? {
279 Frame::ConnectAck { .. } => {}
280 Frame::ConnectError {
281 reason_code,
282 message,
283 ..
284 } => {
285 return Err(connection_error(&format!(
286 "server rejected subscription connection (reason {reason_code}): {}",
287 message.unwrap_or_else(|| "no detail".to_string())
288 )));
289 }
290 other => {
291 return Err(unexpected_setup_frame("ConnectAck", &other));
292 }
293 }
294
295 let subscribe = Frame::Subscribe {
296 flags: 0,
297 stream_id: SUBSCRIPTION_STREAM_ID,
298 channel: channel.to_string(),
299 accepted_schemas,
300 max_in_flight: SUBSCRIBE_MAX_IN_FLIGHT,
301 };
302 let subscription_id =
303 match setup_exchange(&mut socket, &mut driver, &subscribe, &mut pending)? {
304 Frame::SubscribeAck {
305 subscription_id, ..
306 } => subscription_id,
307 Frame::SubscribeError {
308 reason_code,
309 message,
310 ..
311 } => {
312 return Err(SdkError::Protocol {
313 description: format!(
314 "server rejected subscribe (reason {reason_code}): {}",
315 message.unwrap_or_else(|| "no detail".to_string())
316 ),
317 });
318 }
319 other => {
320 return Err(unexpected_setup_frame("SubscribeAck", &other));
321 }
322 };
323 Ok((socket, driver, subscription_id, pending))
324 }
325
326 fn start(
328 socket: WsSocket,
329 driver: WebSocketFrameDriver,
330 binding: WebSocketAuthorityBinding,
331 subscription_id: u64,
332 pending: Vec<WebSocketDeliveredMessage>,
333 ) -> Result<Self, SdkError> {
334 socket.set_read_timeout(None)?;
340 let shutdown = socket.try_clone_stream()?;
341 let binding = Arc::new(Mutex::new(binding));
342 let reader_binding = Arc::clone(&binding);
343 let (sender, inbound) = mpsc::channel();
344 let reader = std::thread::Builder::new()
345 .name("liminal-ws-subscription-reader".to_string())
346 .spawn(move || run_reader(socket, driver, &reader_binding, pending, &sender))
347 .map_err(|source| SdkError::Protocol {
348 description: format!(
349 "failed to start websocket subscription reader thread: {source}"
350 ),
351 })?;
352 Ok(Self {
353 shutdown,
354 subscription_id,
355 inbound,
356 binding,
357 reader: Some(reader),
358 })
359 }
360}
361
362impl Drop for WebSocketSubscriptionStream {
363 fn drop(&mut self) {
364 self.shutdown.shutdown(std::net::Shutdown::Both).ok();
369 if let Some(reader) = self.reader.take() {
370 reader.join().ok();
371 }
372 }
373}
374
375fn setup_exchange(
378 socket: &mut WsSocket,
379 driver: &mut WebSocketFrameDriver,
380 request: &Frame,
381 pending: &mut Vec<WebSocketDeliveredMessage>,
382) -> Result<Frame, SdkError> {
383 let bytes = super::encode_frame(request)?;
384 let command = driver
385 .command_send(bytes, ResponseExpectation::Correlated)
386 .map_err(|refusal| SdkError::Protocol {
387 description: format!("subscription driver refused the setup send: {refusal:?}"),
388 })?;
389 let SocketCommand::SendBinary(payload) = command else {
390 return Err(SdkError::Protocol {
391 description: "subscription driver emitted a non-send command for a send".to_string(),
392 });
393 };
394 if let Err(failure) = socket.send_binary(payload) {
395 let step = driver.handle_event(SocketEvent::Failed(failure));
396 if step.command == Some(SocketCommand::Close) {
397 socket.execute_close();
398 }
399 return Err(connection_error(&format!(
400 "failed to send subscription setup frame: {}",
401 socket
402 .last_failure_detail()
403 .unwrap_or("websocket send failed")
404 )));
405 }
406 loop {
407 let event = match socket.read_event() {
408 SocketRead::TimedOut => {
409 return Err(connection_error(
410 "subscription connection timed out waiting for a control-frame reply",
411 ));
412 }
413 SocketRead::Event(event) => event,
414 };
415 let step = driver.handle_event(event);
416 if step.command == Some(SocketCommand::Close) {
417 socket.execute_close();
418 }
419 match step.output {
420 DriverOutput::Frame { bytes, correlation } => {
421 let frame = decode_message(&bytes)?;
422 match correlation {
423 FrameCorrelation::UnsolicitedDelivery => {
424 if let Some(message) = delivered_message(frame) {
425 pending.push(message);
426 }
427 }
428 FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
429 return Ok(frame);
430 }
431 }
432 }
433 DriverOutput::Terminal(terminal) => {
434 return Err(connection_error(&format!(
435 "subscription connection terminated during setup: {terminal:?}"
436 )));
437 }
438 DriverOutput::Opened
439 | DriverOutput::PostTerminalIgnored(_)
440 | DriverOutput::Refused(_) => {
441 return Err(SdkError::Protocol {
442 description: format!(
443 "subscription driver produced an unexpected setup output: {:?}",
444 step.output
445 ),
446 });
447 }
448 }
449 }
450}
451
452fn run_reader(
456 mut socket: WsSocket,
457 mut driver: WebSocketFrameDriver,
458 binding: &Mutex<WebSocketAuthorityBinding>,
459 pending: Vec<WebSocketDeliveredMessage>,
460 sender: &Sender<Result<WebSocketDeliveredMessage, SdkError>>,
461) {
462 for message in pending {
463 if sender.send(Ok(message)).is_err() {
464 close_link(&mut socket, &mut driver);
465 return;
466 }
467 }
468 loop {
469 let event = match socket.read_event() {
470 SocketRead::TimedOut => continue,
474 SocketRead::Event(event) => event,
475 };
476 let step = driver.handle_event(event);
477 if step.command == Some(SocketCommand::Close) {
478 socket.execute_close();
479 }
480 match step.output {
481 DriverOutput::Frame { bytes, correlation } => match correlation {
482 FrameCorrelation::UnsolicitedDelivery => {
483 let Ok(frame) = decode_message(&bytes) else {
484 close_link(&mut socket, &mut driver);
486 continue;
487 };
488 if let Some(message) = delivered_message(frame) {
489 if sender.send(Ok(message)).is_err() {
490 close_link(&mut socket, &mut driver);
491 return;
492 }
493 }
494 }
495 FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
496 match decode_message(&bytes) {
497 Ok(Frame::Disconnect { .. }) => {
498 close_link(&mut socket, &mut driver);
502 }
503 Ok(Frame::SubscribeError {
513 reason_code,
514 message,
515 ..
516 }) => {
517 let _sent =
518 sender.send(Err(subscription_ended(reason_code, message)));
519 close_link(&mut socket, &mut driver);
520 }
521 Ok(_) => {}
525 Err(_) => {
526 close_link(&mut socket, &mut driver);
527 }
528 }
529 }
530 },
531 DriverOutput::Terminal(terminal) => {
532 let _outcome = binding.lock().established_terminal(&terminal);
535 return;
536 }
537 DriverOutput::PostTerminalIgnored(_) => return,
538 DriverOutput::Opened | DriverOutput::Refused(_) => {}
539 }
540 }
541}
542
543fn close_link(socket: &mut WsSocket, driver: &mut WebSocketFrameDriver) {
545 if driver.command_close().is_ok() {
546 socket.execute_close();
547 }
548}
549
550fn decode_message(bytes: &[u8]) -> Result<Frame, SdkError> {
552 match decode(bytes) {
553 Ok((frame, consumed)) if consumed == bytes.len() => Ok(frame),
554 Ok((_, consumed)) => Err(SdkError::Protocol {
555 description: format!(
556 "subscription decode consumed {consumed} of {} message bytes",
557 bytes.len()
558 ),
559 }),
560 Err(error) => Err(SdkError::Protocol {
561 description: format!("subscription wire codec error: {error}"),
562 }),
563 }
564}
565
566fn delivered_message(frame: Frame) -> Option<WebSocketDeliveredMessage> {
568 match frame {
569 Frame::Deliver {
570 delivery_seq,
571 envelope,
572 ..
573 } => Some(WebSocketDeliveredMessage {
574 delivery_seq,
575 schema_id: envelope.schema_id,
576 payload: envelope.payload,
577 }),
578 _ => None,
579 }
580}
581
582fn subscription_ended(reason_code: u16, message: Option<alloc::string::String>) -> SdkError {
588 SdkError::Protocol {
589 description: format!(
590 "server ended the subscription (reason {reason_code}): {}",
591 message.unwrap_or_else(|| "no detail".to_string())
592 ),
593 }
594}
595
596fn unexpected_setup_frame(expected: &str, actual: &Frame) -> SdkError {
598 SdkError::Protocol {
599 description: format!(
600 "expected {expected} during subscription setup, received {:?}",
601 actual.frame_type()
602 ),
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::SETUP_TIMEOUT;
609 use core::time::Duration;
610
611 #[test]
619 fn websocket_subscription_source_has_no_retired_reader_poll_family() {
620 const SOURCE: &str = include_str!("subscription.rs");
621 let production = SOURCE.split("#[cfg(test)]").next().unwrap_or(SOURCE);
622 for forbidden in [
623 "READER_POLL_TIMEOUT",
624 "AtomicBool",
625 "stop.load",
626 "stop.store",
627 "re-check the stop flag",
628 "poll the stop flag",
629 ] {
630 assert!(
631 !production.contains(forbidden),
632 "retired websocket-subscription-reader poll-family source \
633 `{forbidden}` reappeared"
634 );
635 }
636 }
637
638 #[test]
642 fn the_named_setup_deadline_is_the_ratified_five_seconds() {
643 assert_eq!(SETUP_TIMEOUT, Duration::from_secs(5));
644 }
645}