Skip to main content

liminal_sdk/remote/websocket/
core.rs

1//! Transport-neutral, event-driven WebSocket liminal driver (R3.1).
2//!
3//! The driver's inputs are the closed socket events an adapter observes
4//! ([`SocketEvent`]) and its outputs are the closed commands an adapter
5//! executes ([`SocketCommand`]) plus typed transport fates. It owns
6//! canonical-frame validation (exactly one self-delimiting liminal frame per
7//! binary message, bounded by the named product limit) and the in-flight wire
8//! correlation state (at most one outstanding correlated exchange, the Q-B
9//! rule). It is `no_std + alloc` and contains no platform type: the blocking
10//! std adapter and the later browser adapter drive the same commands, so
11//! neither owns reconnect, retry, replay, or correlation policy.
12//!
13//! F3 terminal discipline: the FIRST terminal event mints exactly one typed
14//! [`TransportTerminal`]; every later terminal (the browser's `close` echo
15//! after `error`, a duplicate close, a late message) is a typed no-op
16//! ([`DriverOutput::PostTerminalIgnored`]) and never reaches the client unit.
17
18use alloc::vec::Vec;
19
20use liminal_protocol::wire::{FRAME_MAX, GENERIC_HEADER_LEN};
21
22/// Wire discriminant of the server `Deliver` frame in the canonical registry.
23///
24/// The driver is `no_std` and cannot depend on the std-bound `liminal` crate
25/// that owns `FrameType`; the parity suite pins this byte against
26/// `FrameType::Deliver` so drift is impossible without a red test.
27const FRAME_TYPE_DELIVER: u8 = 0x19;
28
29/// Byte offset of the big-endian `u32` payload length in the generic header.
30const PAYLOAD_LENGTH_OFFSET: usize = 6;
31
32/// Closed socket-failure classes an adapter may report.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SocketFailure {
35    /// The socket failed at the transport layer (I/O or WebSocket protocol).
36    Transport,
37    /// The peer sent a text message; the wire contract admits only binary.
38    UnsupportedTextMessage,
39    /// The peer declared a message beyond the pinned reassembly bound (F2).
40    MessageBeyondBound,
41}
42
43/// Closed socket events an adapter feeds into the driver.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum SocketEvent {
46    /// The socket completed its transport-level open.
47    Opened,
48    /// One complete, reassembled binary message.
49    Binary(Vec<u8>),
50    /// The socket closed cleanly.
51    Closed,
52    /// The socket failed with a typed failure class.
53    Failed(SocketFailure),
54}
55
56/// Closed commands the driver emits for an adapter to execute.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum SocketCommand {
59    /// Open the transport-level socket.
60    Open,
61    /// Send one complete binary message.
62    SendBinary(Vec<u8>),
63    /// Close the transport-level socket.
64    Close,
65}
66
67/// Typed canonical-frame violations the driver detects on inbound messages.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum FrameViolation {
70    /// The binary message carried no bytes at all.
71    EmptyMessage,
72    /// The message ended before the ten-byte generic header completed.
73    TruncatedHeader {
74        /// Bytes actually present.
75        length: usize,
76    },
77    /// The declared complete frame exceeds the active liminal frame bound.
78    DeclaredBeyondBound {
79        /// Declared complete-frame size in bytes.
80        declared_total: u64,
81        /// The active bound the declaration exceeded.
82        bound: u64,
83    },
84    /// The message ended before the declared body completed.
85    TruncatedBody {
86        /// Declared complete-frame size in bytes.
87        declared_total: u64,
88        /// Bytes actually present.
89        actual: u64,
90    },
91    /// The message carried bytes past the declared frame (including a second
92    /// concatenated frame).
93    TrailingBytes {
94        /// Declared complete-frame size in bytes.
95        declared_total: u64,
96        /// Bytes actually present.
97        actual: u64,
98    },
99    /// The framing was valid but the canonical codec refused the body.
100    ///
101    /// This variant is minted by the codec-owning adapter layer, never by the
102    /// `no_std` driver (which validates the self-delimiting header shape and
103    /// deliberately does not decode bodies).
104    UndecodableBody,
105}
106
107/// The one typed terminal fate a driver lifetime mints (F3).
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum TransportTerminal {
110    /// The peer closed the connection cleanly without a local close command.
111    PeerClosed,
112    /// A locally commanded close completed with the echoed close event.
113    CloseCompleted,
114    /// The socket failed with a typed failure class.
115    SocketFailed(SocketFailure),
116    /// An inbound message violated the canonical-frame contract.
117    ProtocolViolation(FrameViolation),
118}
119
120/// Driver lifecycle phase, exposed read-only for adapters and tests.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum DriverPhase {
123    /// No open has been commanded.
124    Idle,
125    /// The open command was emitted and no `Opened` event has arrived.
126    Opening,
127    /// The socket is established.
128    Established,
129    /// A close was commanded and its echoed close event is pending.
130    Closing,
131    /// A terminal fate was minted; every further event is a typed no-op.
132    Terminated,
133}
134
135/// Correlation class of one validated inbound frame.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum FrameCorrelation {
138    /// The frame resolves the single outstanding correlated exchange.
139    CorrelatedResponse,
140    /// A server `Deliver` frame, never correlated to an exchange.
141    UnsolicitedDelivery,
142    /// A non-`Deliver` frame that arrived with no exchange outstanding.
143    UnsolicitedFrame,
144}
145
146/// Kind of event observed after the terminal fate was already minted.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum PostTerminalEvent {
149    /// A late `Opened` event.
150    Opened,
151    /// A late binary message.
152    Binary,
153    /// A late close event (the F3 `error`-then-`close` echo).
154    Closed,
155    /// A late failure event.
156    Failed,
157}
158
159/// Typed refusal for an event that is illegal in the current phase.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum EventRefusal {
162    /// `Opened` arrived while no open command was outstanding.
163    OpenedWithoutOpenCommand,
164    /// A binary message arrived before the socket was established.
165    BinaryBeforeEstablished,
166}
167
168/// Whether an outbound send expects a correlated response frame.
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum ResponseExpectation {
171    /// Fire-and-forget: no response frame is correlated to this send.
172    None,
173    /// The next non-`Deliver` inbound frame resolves this exchange.
174    Correlated,
175}
176
177/// Typed refusal for a command that is illegal in the current driver state.
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub enum CommandRefusal {
180    /// The command is not legal in the current phase.
181    InvalidPhase {
182        /// The refusing phase.
183        phase: DriverPhase,
184    },
185    /// A correlated exchange is already outstanding (the Q-B rule).
186    ExchangeOutstanding,
187}
188
189/// One decision output of the driver.
190#[derive(Clone, Debug, PartialEq, Eq)]
191pub enum DriverOutput {
192    /// The socket proved open; the binding may record `Connected`.
193    Opened,
194    /// One validated canonical frame with its correlation class.
195    Frame {
196        /// The exact canonical frame bytes (header plus declared body).
197        bytes: Vec<u8>,
198        /// Correlation class of this frame.
199        correlation: FrameCorrelation,
200    },
201    /// The one typed terminal fate of this driver lifetime.
202    Terminal(TransportTerminal),
203    /// A typed post-terminal no-op (F3); never reaches the client unit.
204    PostTerminalIgnored(PostTerminalEvent),
205    /// A typed refusal of an event illegal in the current phase.
206    Refused(EventRefusal),
207}
208
209/// One driver step: the decision output plus at most one emitted command.
210#[derive(Clone, Debug, PartialEq, Eq)]
211#[must_use]
212pub struct DriverStep {
213    /// The decision output for the handled event.
214    pub output: DriverOutput,
215    /// A command the adapter must execute, when one was emitted.
216    pub command: Option<SocketCommand>,
217}
218
219impl DriverStep {
220    const fn output(output: DriverOutput) -> Self {
221        Self {
222            output,
223            command: None,
224        }
225    }
226
227    const fn with_command(output: DriverOutput, command: SocketCommand) -> Self {
228        Self {
229            output,
230            command: Some(command),
231        }
232    }
233}
234
235/// Transport-neutral WebSocket liminal driver.
236///
237/// One driver owns one socket lifetime: it opens at most once and mints at
238/// most one terminal fate. Reconnecting means a fresh driver under a fresh
239/// aggregate-issued authorization — the driver itself can never re-open.
240#[derive(Debug)]
241pub struct WebSocketFrameDriver {
242    phase: DriverPhase,
243    exchange_outstanding: bool,
244}
245
246impl WebSocketFrameDriver {
247    /// Creates an idle driver bound to the canonical liminal frame bound.
248    #[must_use]
249    pub const fn new() -> Self {
250        Self {
251            phase: DriverPhase::Idle,
252            exchange_outstanding: false,
253        }
254    }
255
256    /// Current lifecycle phase.
257    #[must_use]
258    pub const fn phase(&self) -> DriverPhase {
259        self.phase
260    }
261
262    /// Reports whether one correlated exchange is outstanding.
263    #[must_use]
264    pub const fn has_outstanding_exchange(&self) -> bool {
265        self.exchange_outstanding
266    }
267
268    /// Emits the single open command of this driver lifetime.
269    ///
270    /// # Errors
271    ///
272    /// Returns [`CommandRefusal::InvalidPhase`] unless the driver is idle.
273    pub const fn command_open(&mut self) -> Result<SocketCommand, CommandRefusal> {
274        match self.phase {
275            DriverPhase::Idle => {
276                self.phase = DriverPhase::Opening;
277                Ok(SocketCommand::Open)
278            }
279            DriverPhase::Opening
280            | DriverPhase::Established
281            | DriverPhase::Closing
282            | DriverPhase::Terminated => Err(CommandRefusal::InvalidPhase { phase: self.phase }),
283        }
284    }
285
286    /// Emits a send command for one canonical frame image.
287    ///
288    /// # Errors
289    ///
290    /// Returns [`CommandRefusal::InvalidPhase`] unless the socket is
291    /// established, and [`CommandRefusal::ExchangeOutstanding`] when a
292    /// correlated exchange is already in flight (the Q-B rule).
293    pub fn command_send(
294        &mut self,
295        bytes: Vec<u8>,
296        expectation: ResponseExpectation,
297    ) -> Result<SocketCommand, CommandRefusal> {
298        if self.phase != DriverPhase::Established {
299            return Err(CommandRefusal::InvalidPhase { phase: self.phase });
300        }
301        match expectation {
302            ResponseExpectation::Correlated => {
303                if self.exchange_outstanding {
304                    return Err(CommandRefusal::ExchangeOutstanding);
305                }
306                self.exchange_outstanding = true;
307            }
308            ResponseExpectation::None => {}
309        }
310        Ok(SocketCommand::SendBinary(bytes))
311    }
312
313    /// Emits the close command, moving the driver into the closing phase.
314    ///
315    /// The commanded close is not itself terminal: the echoed close event
316    /// mints [`TransportTerminal::CloseCompleted`] (F3).
317    ///
318    /// # Errors
319    ///
320    /// Returns [`CommandRefusal::InvalidPhase`] unless the socket is opening
321    /// or established.
322    pub const fn command_close(&mut self) -> Result<SocketCommand, CommandRefusal> {
323        match self.phase {
324            DriverPhase::Opening | DriverPhase::Established => {
325                self.phase = DriverPhase::Closing;
326                Ok(SocketCommand::Close)
327            }
328            DriverPhase::Idle | DriverPhase::Closing | DriverPhase::Terminated => {
329                Err(CommandRefusal::InvalidPhase { phase: self.phase })
330            }
331        }
332    }
333
334    /// Handles one closed socket event and returns the driver's decision.
335    pub fn handle_event(&mut self, event: SocketEvent) -> DriverStep {
336        if self.phase == DriverPhase::Terminated {
337            let kind = match event {
338                SocketEvent::Opened => PostTerminalEvent::Opened,
339                SocketEvent::Binary(_) => PostTerminalEvent::Binary,
340                SocketEvent::Closed => PostTerminalEvent::Closed,
341                SocketEvent::Failed(_) => PostTerminalEvent::Failed,
342            };
343            return DriverStep::output(DriverOutput::PostTerminalIgnored(kind));
344        }
345        match event {
346            SocketEvent::Opened => self.handle_opened(),
347            SocketEvent::Binary(bytes) => self.handle_binary(bytes),
348            SocketEvent::Closed => self.handle_closed(),
349            SocketEvent::Failed(failure) => self.handle_failed(failure),
350        }
351    }
352
353    const fn handle_opened(&mut self) -> DriverStep {
354        match self.phase {
355            DriverPhase::Opening => {
356                self.phase = DriverPhase::Established;
357                DriverStep::output(DriverOutput::Opened)
358            }
359            DriverPhase::Idle | DriverPhase::Established | DriverPhase::Closing => {
360                DriverStep::output(DriverOutput::Refused(
361                    EventRefusal::OpenedWithoutOpenCommand,
362                ))
363            }
364            DriverPhase::Terminated => unreachable_terminated(),
365        }
366    }
367
368    fn handle_binary(&mut self, bytes: Vec<u8>) -> DriverStep {
369        match self.phase {
370            DriverPhase::Established | DriverPhase::Closing => match validate_frame(&bytes) {
371                Ok(()) => {
372                    let correlation = self.classify_frame(&bytes);
373                    DriverStep::output(DriverOutput::Frame { bytes, correlation })
374                }
375                Err(violation) => self.mint_terminal(
376                    TransportTerminal::ProtocolViolation(violation),
377                    // A violation from the established phase must actively
378                    // close the socket; after a commanded close it is already
379                    // closing, so no second close command is emitted.
380                    self.phase == DriverPhase::Established,
381                ),
382            },
383            DriverPhase::Idle | DriverPhase::Opening => {
384                DriverStep::output(DriverOutput::Refused(EventRefusal::BinaryBeforeEstablished))
385            }
386            DriverPhase::Terminated => unreachable_terminated(),
387        }
388    }
389
390    const fn handle_closed(&mut self) -> DriverStep {
391        let terminal = match self.phase {
392            DriverPhase::Closing => TransportTerminal::CloseCompleted,
393            DriverPhase::Idle | DriverPhase::Opening | DriverPhase::Established => {
394                TransportTerminal::PeerClosed
395            }
396            DriverPhase::Terminated => return unreachable_terminated(),
397        };
398        self.mint_terminal(terminal, false)
399    }
400
401    const fn handle_failed(&mut self, failure: SocketFailure) -> DriverStep {
402        let emit_close = !matches!(self.phase, DriverPhase::Closing);
403        self.mint_terminal(TransportTerminal::SocketFailed(failure), emit_close)
404    }
405
406    /// Mints the single terminal fate and optionally commands a socket close.
407    const fn mint_terminal(&mut self, terminal: TransportTerminal, emit_close: bool) -> DriverStep {
408        self.phase = DriverPhase::Terminated;
409        self.exchange_outstanding = false;
410        if emit_close {
411            DriverStep::with_command(DriverOutput::Terminal(terminal), SocketCommand::Close)
412        } else {
413            DriverStep::output(DriverOutput::Terminal(terminal))
414        }
415    }
416
417    fn classify_frame(&mut self, bytes: &[u8]) -> FrameCorrelation {
418        if bytes.first().copied() == Some(FRAME_TYPE_DELIVER) {
419            return FrameCorrelation::UnsolicitedDelivery;
420        }
421        if self.exchange_outstanding {
422            self.exchange_outstanding = false;
423            return FrameCorrelation::CorrelatedResponse;
424        }
425        FrameCorrelation::UnsolicitedFrame
426    }
427}
428
429impl Default for WebSocketFrameDriver {
430    fn default() -> Self {
431        Self::new()
432    }
433}
434
435/// The terminated phase is filtered before per-event handling; reaching a
436/// per-event handler terminated would be a driver defect, and reporting it as
437/// a typed post-terminal no-op keeps the closed output contract intact.
438const fn unreachable_terminated() -> DriverStep {
439    DriverStep::output(DriverOutput::PostTerminalIgnored(PostTerminalEvent::Failed))
440}
441
442/// Validates that `bytes` hold exactly one canonical liminal frame.
443///
444/// The generic frame is self-delimiting: a ten-byte header whose bytes
445/// `6..10` carry the big-endian `u32` payload length, so one complete frame
446/// occupies exactly `10 + payload_length` bytes. The complete size is bounded
447/// by the named product limit [`FRAME_MAX`].
448fn validate_frame(bytes: &[u8]) -> Result<(), FrameViolation> {
449    if bytes.is_empty() {
450        return Err(FrameViolation::EmptyMessage);
451    }
452    if bytes.len() < GENERIC_HEADER_LEN {
453        return Err(FrameViolation::TruncatedHeader {
454            length: bytes.len(),
455        });
456    }
457    let Some(length_bytes) = bytes.get(PAYLOAD_LENGTH_OFFSET..GENERIC_HEADER_LEN) else {
458        return Err(FrameViolation::TruncatedHeader {
459            length: bytes.len(),
460        });
461    };
462    let Ok(length_array) = <[u8; 4]>::try_from(length_bytes) else {
463        return Err(FrameViolation::TruncatedHeader {
464            length: bytes.len(),
465        });
466    };
467    let declared_payload = u64::from(u32::from_be_bytes(length_array));
468    let Ok(header_len) = u64::try_from(GENERIC_HEADER_LEN) else {
469        // The ten-byte header length always fits `u64`; a target where it did
470        // not could not validate any frame, so the message is refused typed.
471        return Err(FrameViolation::TruncatedHeader {
472            length: bytes.len(),
473        });
474    };
475    let declared_total = declared_payload.saturating_add(header_len);
476    if declared_total > FRAME_MAX {
477        return Err(FrameViolation::DeclaredBeyondBound {
478            declared_total,
479            bound: FRAME_MAX,
480        });
481    }
482    // A message longer than `u64::MAX` bytes cannot exist on any real target;
483    // saturating keeps the comparison honest (it exceeds every declaration).
484    let actual = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
485    if actual < declared_total {
486        return Err(FrameViolation::TruncatedBody {
487            declared_total,
488            actual,
489        });
490    }
491    if actual > declared_total {
492        return Err(FrameViolation::TrailingBytes {
493            declared_total,
494            actual,
495        });
496    }
497    Ok(())
498}