liminal_sdk/remote/websocket/web_socket/mirror.rs
1//! Platform-neutral browser-fact mirror for the wasm adapter (R3.2, F5).
2//!
3//! This module is the adapter's entire conversion layer: it maps the four
4//! browser socket callbacks (`open`, `message`, `close`, `error`) onto the
5//! driver's closed [`SocketEvent`] set, and maps emitted [`SocketCommand`]s
6//! onto the closed [`BrowserSocketAction`] set the `web-sys` shim executes.
7//!
8//! F5 mirror-not-implementor: every function here is pure and stateless. The
9//! mirror never validates frames, never correlates exchanges, never decides a
10//! fate, and never retries — those judgments belong exclusively to the
11//! transport-neutral driver core. Because no platform type appears here, the
12//! whole layer compiles natively and is pinned by the deterministic
13//! `ws_browser_mirror_trace` suite without a browser.
14
15use alloc::vec::Vec;
16
17use super::super::core::{SocketCommand, SocketEvent, SocketFailure};
18
19/// Classified payload of one browser `message` event, as the `web-sys` shim
20/// observes it after F4 pinned `binaryType = "arraybuffer"` at construction.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum BrowserMessageData {
23 /// One complete binary message delivered as an `ArrayBuffer` — the only
24 /// data shape the transport accepts (F4). The browser has already
25 /// reassembled fragmented frames into this single complete message.
26 ArrayBuffer(Vec<u8>),
27 /// A text (string) message. The liminal wire contract admits only binary.
28 Text,
29 /// A `Blob` — the browser's asynchronous binary default, which F4 rules
30 /// out by pinning `binaryType` before any message can arrive. Observing
31 /// one means the transport contract broke; its contents are never read.
32 Blob,
33 /// Message data of any other JavaScript type.
34 Unrecognized,
35}
36
37/// Converts one classified browser `message` event into the driver's event.
38///
39/// Text surfaces as the core's typed
40/// [`SocketFailure::UnsupportedTextMessage`]; `Blob` and unrecognized data
41/// are typed transport failures (F4). The mirror performs no frame
42/// validation: canonical-frame judgment on `ArrayBuffer` bytes is the core's
43/// alone (F5).
44#[must_use]
45pub fn message_event(data: BrowserMessageData) -> SocketEvent {
46 match data {
47 BrowserMessageData::ArrayBuffer(bytes) => SocketEvent::Binary(bytes),
48 BrowserMessageData::Text => SocketEvent::Failed(SocketFailure::UnsupportedTextMessage),
49 BrowserMessageData::Blob | BrowserMessageData::Unrecognized => {
50 SocketEvent::Failed(SocketFailure::Transport)
51 }
52 }
53}
54
55/// Converts the browser `open` event, checking the F1 extension posture.
56///
57/// `negotiated_extensions` is the socket's `extensions` attribute at `open`
58/// time. The landed acceptor declines every extension offer
59/// (decline-never-negotiate), so extension-free operation is guaranteed by
60/// the server's F1 posture — the browser cannot be configured to withhold
61/// its offers. A non-empty negotiated string therefore proves the peer is
62/// not honoring the liminal transport contract: the mirror reports a typed
63/// transport failure and the driver mints the fate and closes the socket.
64#[must_use]
65pub const fn open_event(negotiated_extensions: &str) -> SocketEvent {
66 if negotiated_extensions.is_empty() {
67 SocketEvent::Opened
68 } else {
69 SocketEvent::Failed(SocketFailure::Transport)
70 }
71}
72
73/// Converts the browser `close` event with close-code fidelity.
74///
75/// A clean close (`wasClean == true`) is the orderly close fact,
76/// [`SocketEvent::Closed`]. An abnormal close (`wasClean == false`) is a
77/// loss fact, not a clean peer close: it converts to a typed transport
78/// failure so a lone abnormal close can never misrepresent itself as
79/// orderly. In F3's browser shape abnormal loss fires `error` first, so this
80/// abnormal conversion normally lands post-terminal and stays a typed no-op.
81/// The numeric close code and reason string carry no additional decision
82/// weight; the shim retains them as diagnostics only.
83#[must_use]
84pub const fn close_event(was_clean: bool) -> SocketEvent {
85 if was_clean {
86 SocketEvent::Closed
87 } else {
88 SocketEvent::Failed(SocketFailure::Transport)
89 }
90}
91
92/// Converts the browser `error` event.
93///
94/// Browser error events are opaque by specification — they deliberately
95/// carry no failure detail. The conversion therefore states only the typed
96/// class, [`SocketFailure::Transport`], and invents nothing beyond it.
97#[must_use]
98pub const fn error_event() -> SocketEvent {
99 SocketEvent::Failed(SocketFailure::Transport)
100}
101
102/// Closed browser socket actions the `web-sys` shim executes.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum BrowserSocketAction {
105 /// Send one complete binary message (`WebSocket.send`).
106 SendBinary(Vec<u8>),
107 /// Close the socket (`WebSocket.close`).
108 Close,
109}
110
111/// Typed refusal for a driver command with no runtime browser action.
112#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
113pub enum BrowserCommandRefusal {
114 /// A browser socket begins connecting when it is constructed, so the
115 /// driver's single `Open` command is executed by construction itself; a
116 /// runtime `Open` mapping would be a second connect and is refused.
117 #[error("the browser socket opens at construction; Open is not a runtime action")]
118 OpenIsConstruction,
119}
120
121/// Maps one driver command onto the browser action that executes it.
122///
123/// # Errors
124///
125/// Returns [`BrowserCommandRefusal::OpenIsConstruction`] for
126/// [`SocketCommand::Open`]: construction of the browser socket is the one
127/// execution of that command.
128pub fn action_for_command(
129 command: SocketCommand,
130) -> Result<BrowserSocketAction, BrowserCommandRefusal> {
131 match command {
132 SocketCommand::Open => Err(BrowserCommandRefusal::OpenIsConstruction),
133 SocketCommand::SendBinary(bytes) => Ok(BrowserSocketAction::SendBinary(bytes)),
134 SocketCommand::Close => Ok(BrowserSocketAction::Close),
135 }
136}