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<WebSocketDeliveredMessage>,
97 binding: Arc<Mutex<WebSocketAuthorityBinding>>,
98 reader: Option<JoinHandle<()>>,
99}
100
101impl WebSocketSubscriptionStream {
102 pub fn open(
114 address: &str,
115 channel: &str,
116 accepted_schemas: Vec<SchemaId>,
117 ) -> Result<Self, SdkError> {
118 Self::open_with_auth(address, channel, accepted_schemas, &[])
119 }
120
121 pub fn open_with_auth(
142 address: &str,
143 channel: &str,
144 accepted_schemas: Vec<SchemaId>,
145 auth_token: &[u8],
146 ) -> Result<Self, SdkError> {
147 let message_bound = liminal_ws_message_bound()?;
148 let mut binding = WebSocketAuthorityBinding::new();
149 match binding.request_open() {
150 OpenRequestDecision::Authorized { .. } => {}
151 OpenRequestDecision::Refused(refusal) => {
152 return Err(connection_error(&format!(
153 "client authority refused the subscription open: {refusal:?}"
154 )));
155 }
156 }
157 match Self::open_link(
158 address,
159 channel,
160 accepted_schemas,
161 message_bound,
162 auth_token,
163 ) {
164 Ok((socket, driver, subscription_id, pending)) => {
165 match binding.connection_established() {
166 AttemptFateOutcome::Recorded { .. } => {}
167 AttemptFateOutcome::Refused(refusal) => {
168 return Err(SdkError::Protocol {
169 description: format!(
170 "client authority refused the Connected fate for the \
171 subscription open: {refusal:?}"
172 ),
173 });
174 }
175 }
176 Self::start(socket, driver, binding, subscription_id, pending)
177 }
178 Err(error) => match binding.open_failed() {
179 AttemptFateOutcome::Recorded { .. } => Err(error),
180 AttemptFateOutcome::Refused(refusal) => Err(SdkError::Protocol {
181 description: format!(
182 "subscription open failed ({error}) and the client authority \
183 refused the Failed fate: {refusal:?}"
184 ),
185 }),
186 },
187 }
188 }
189
190 pub fn recv_timeout(&self, timeout: Duration) -> Result<WebSocketDeliveredMessage, SdkError> {
197 self.inbound.recv_timeout(timeout).map_err(|error| {
198 let detail = match error {
199 RecvTimeoutError::Timeout => "no delivery arrived within the timeout",
200 RecvTimeoutError::Disconnected => {
201 "the subscription reader stopped before a delivery arrived"
202 }
203 };
204 connection_error(&format!("websocket subscription receive failed: {detail}"))
205 })
206 }
207
208 #[must_use]
210 pub const fn subscription_id(&self) -> u64 {
211 self.subscription_id
212 }
213
214 #[must_use]
216 pub fn reconnect_state(&self) -> ReconnectState {
217 self.binding.lock().reconnect_state()
218 }
219
220 fn open_link(
223 address: &str,
224 channel: &str,
225 accepted_schemas: Vec<SchemaId>,
226 message_bound: usize,
227 auth_token: &[u8],
228 ) -> Result<
229 (
230 WsSocket,
231 WebSocketFrameDriver,
232 u64,
233 Vec<WebSocketDeliveredMessage>,
234 ),
235 SdkError,
236 > {
237 let mut driver = WebSocketFrameDriver::new();
238 let command = driver
239 .command_open()
240 .map_err(|refusal| SdkError::Protocol {
241 description: format!("subscription driver refused its first open: {refusal:?}"),
242 })?;
243 if command != SocketCommand::Open {
244 return Err(SdkError::Protocol {
245 description: "subscription driver emitted a non-open first command".to_string(),
246 });
247 }
248 let mut socket = WsSocket::connect(address, message_bound)?;
249 socket.set_read_timeout(Some(SETUP_TIMEOUT))?;
254 let step = driver.handle_event(SocketEvent::Opened);
255 if step.output != DriverOutput::Opened {
256 return Err(SdkError::Protocol {
257 description: format!("subscription driver refused the opened socket: {step:?}"),
258 });
259 }
260
261 let mut pending = Vec::new();
262 let connect = Frame::Connect {
263 flags: 0,
264 min_version: CLIENT_MIN_VERSION,
265 max_version: CLIENT_MAX_VERSION,
266 auth_token: auth_token.to_vec(),
267 };
268 match setup_exchange(&mut socket, &mut driver, &connect, &mut pending)? {
269 Frame::ConnectAck { .. } => {}
270 Frame::ConnectError {
271 reason_code,
272 message,
273 ..
274 } => {
275 return Err(connection_error(&format!(
276 "server rejected subscription connection (reason {reason_code}): {}",
277 message.unwrap_or_else(|| "no detail".to_string())
278 )));
279 }
280 other => {
281 return Err(unexpected_setup_frame("ConnectAck", &other));
282 }
283 }
284
285 let subscribe = Frame::Subscribe {
286 flags: 0,
287 stream_id: SUBSCRIPTION_STREAM_ID,
288 channel: channel.to_string(),
289 accepted_schemas,
290 max_in_flight: SUBSCRIBE_MAX_IN_FLIGHT,
291 };
292 let subscription_id =
293 match setup_exchange(&mut socket, &mut driver, &subscribe, &mut pending)? {
294 Frame::SubscribeAck {
295 subscription_id, ..
296 } => subscription_id,
297 Frame::SubscribeError {
298 reason_code,
299 message,
300 ..
301 } => {
302 return Err(SdkError::Protocol {
303 description: format!(
304 "server rejected subscribe (reason {reason_code}): {}",
305 message.unwrap_or_else(|| "no detail".to_string())
306 ),
307 });
308 }
309 other => {
310 return Err(unexpected_setup_frame("SubscribeAck", &other));
311 }
312 };
313 Ok((socket, driver, subscription_id, pending))
314 }
315
316 fn start(
318 socket: WsSocket,
319 driver: WebSocketFrameDriver,
320 binding: WebSocketAuthorityBinding,
321 subscription_id: u64,
322 pending: Vec<WebSocketDeliveredMessage>,
323 ) -> Result<Self, SdkError> {
324 socket.set_read_timeout(None)?;
330 let shutdown = socket.try_clone_stream()?;
331 let binding = Arc::new(Mutex::new(binding));
332 let reader_binding = Arc::clone(&binding);
333 let (sender, inbound) = mpsc::channel();
334 let reader = std::thread::Builder::new()
335 .name("liminal-ws-subscription-reader".to_string())
336 .spawn(move || run_reader(socket, driver, &reader_binding, pending, &sender))
337 .map_err(|source| SdkError::Protocol {
338 description: format!(
339 "failed to start websocket subscription reader thread: {source}"
340 ),
341 })?;
342 Ok(Self {
343 shutdown,
344 subscription_id,
345 inbound,
346 binding,
347 reader: Some(reader),
348 })
349 }
350}
351
352impl Drop for WebSocketSubscriptionStream {
353 fn drop(&mut self) {
354 self.shutdown.shutdown(std::net::Shutdown::Both).ok();
359 if let Some(reader) = self.reader.take() {
360 reader.join().ok();
361 }
362 }
363}
364
365fn setup_exchange(
368 socket: &mut WsSocket,
369 driver: &mut WebSocketFrameDriver,
370 request: &Frame,
371 pending: &mut Vec<WebSocketDeliveredMessage>,
372) -> Result<Frame, SdkError> {
373 let bytes = super::encode_frame(request)?;
374 let command = driver
375 .command_send(bytes, ResponseExpectation::Correlated)
376 .map_err(|refusal| SdkError::Protocol {
377 description: format!("subscription driver refused the setup send: {refusal:?}"),
378 })?;
379 let SocketCommand::SendBinary(payload) = command else {
380 return Err(SdkError::Protocol {
381 description: "subscription driver emitted a non-send command for a send".to_string(),
382 });
383 };
384 if let Err(failure) = socket.send_binary(payload) {
385 let step = driver.handle_event(SocketEvent::Failed(failure));
386 if step.command == Some(SocketCommand::Close) {
387 socket.execute_close();
388 }
389 return Err(connection_error(&format!(
390 "failed to send subscription setup frame: {}",
391 socket
392 .last_failure_detail()
393 .unwrap_or("websocket send failed")
394 )));
395 }
396 loop {
397 let event = match socket.read_event() {
398 SocketRead::TimedOut => {
399 return Err(connection_error(
400 "subscription connection timed out waiting for a control-frame reply",
401 ));
402 }
403 SocketRead::Event(event) => event,
404 };
405 let step = driver.handle_event(event);
406 if step.command == Some(SocketCommand::Close) {
407 socket.execute_close();
408 }
409 match step.output {
410 DriverOutput::Frame { bytes, correlation } => {
411 let frame = decode_message(&bytes)?;
412 match correlation {
413 FrameCorrelation::UnsolicitedDelivery => {
414 if let Some(message) = delivered_message(frame) {
415 pending.push(message);
416 }
417 }
418 FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
419 return Ok(frame);
420 }
421 }
422 }
423 DriverOutput::Terminal(terminal) => {
424 return Err(connection_error(&format!(
425 "subscription connection terminated during setup: {terminal:?}"
426 )));
427 }
428 DriverOutput::Opened
429 | DriverOutput::PostTerminalIgnored(_)
430 | DriverOutput::Refused(_) => {
431 return Err(SdkError::Protocol {
432 description: format!(
433 "subscription driver produced an unexpected setup output: {:?}",
434 step.output
435 ),
436 });
437 }
438 }
439 }
440}
441
442fn run_reader(
446 mut socket: WsSocket,
447 mut driver: WebSocketFrameDriver,
448 binding: &Mutex<WebSocketAuthorityBinding>,
449 pending: Vec<WebSocketDeliveredMessage>,
450 sender: &Sender<WebSocketDeliveredMessage>,
451) {
452 for message in pending {
453 if sender.send(message).is_err() {
454 close_link(&mut socket, &mut driver);
455 return;
456 }
457 }
458 loop {
459 let event = match socket.read_event() {
460 SocketRead::TimedOut => continue,
464 SocketRead::Event(event) => event,
465 };
466 let step = driver.handle_event(event);
467 if step.command == Some(SocketCommand::Close) {
468 socket.execute_close();
469 }
470 match step.output {
471 DriverOutput::Frame { bytes, correlation } => match correlation {
472 FrameCorrelation::UnsolicitedDelivery => {
473 let Ok(frame) = decode_message(&bytes) else {
474 close_link(&mut socket, &mut driver);
476 continue;
477 };
478 if let Some(message) = delivered_message(frame) {
479 if sender.send(message).is_err() {
480 close_link(&mut socket, &mut driver);
481 return;
482 }
483 }
484 }
485 FrameCorrelation::CorrelatedResponse | FrameCorrelation::UnsolicitedFrame => {
486 match decode_message(&bytes) {
487 Ok(Frame::Disconnect { .. }) => {
488 close_link(&mut socket, &mut driver);
492 }
493 Ok(_) => {}
497 Err(_) => {
498 close_link(&mut socket, &mut driver);
499 }
500 }
501 }
502 },
503 DriverOutput::Terminal(terminal) => {
504 let _outcome = binding.lock().established_terminal(&terminal);
507 return;
508 }
509 DriverOutput::PostTerminalIgnored(_) => return,
510 DriverOutput::Opened | DriverOutput::Refused(_) => {}
511 }
512 }
513}
514
515fn close_link(socket: &mut WsSocket, driver: &mut WebSocketFrameDriver) {
517 if driver.command_close().is_ok() {
518 socket.execute_close();
519 }
520}
521
522fn decode_message(bytes: &[u8]) -> Result<Frame, SdkError> {
524 match decode(bytes) {
525 Ok((frame, consumed)) if consumed == bytes.len() => Ok(frame),
526 Ok((_, consumed)) => Err(SdkError::Protocol {
527 description: format!(
528 "subscription decode consumed {consumed} of {} message bytes",
529 bytes.len()
530 ),
531 }),
532 Err(error) => Err(SdkError::Protocol {
533 description: format!("subscription wire codec error: {error}"),
534 }),
535 }
536}
537
538fn delivered_message(frame: Frame) -> Option<WebSocketDeliveredMessage> {
540 match frame {
541 Frame::Deliver {
542 delivery_seq,
543 envelope,
544 ..
545 } => Some(WebSocketDeliveredMessage {
546 delivery_seq,
547 schema_id: envelope.schema_id,
548 payload: envelope.payload,
549 }),
550 _ => None,
551 }
552}
553
554fn unexpected_setup_frame(expected: &str, actual: &Frame) -> SdkError {
556 SdkError::Protocol {
557 description: format!(
558 "expected {expected} during subscription setup, received {:?}",
559 actual.frame_type()
560 ),
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::SETUP_TIMEOUT;
567 use core::time::Duration;
568
569 #[test]
577 fn websocket_subscription_source_has_no_retired_reader_poll_family() {
578 const SOURCE: &str = include_str!("subscription.rs");
579 let production = SOURCE.split("#[cfg(test)]").next().unwrap_or(SOURCE);
580 for forbidden in [
581 "READER_POLL_TIMEOUT",
582 "AtomicBool",
583 "stop.load",
584 "stop.store",
585 "re-check the stop flag",
586 "poll the stop flag",
587 ] {
588 assert!(
589 !production.contains(forbidden),
590 "retired websocket-subscription-reader poll-family source \
591 `{forbidden}` reappeared"
592 );
593 }
594 }
595
596 #[test]
600 fn the_named_setup_deadline_is_the_ratified_five_seconds() {
601 assert_eq!(SETUP_TIMEOUT, Duration::from_secs(5));
602 }
603}