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