Skip to main content

liminal_sdk/
remote.rs

1mod config;
2mod handles;
3mod participant;
4mod protocol;
5#[cfg(feature = "std")]
6mod tcp;
7pub mod websocket;
8
9#[cfg(feature = "std")]
10pub use tcp::{
11    DeliveredMessage, FlushMode, FlushOutcome, OBSERVABILITY_CHANNEL, PublishRejection, PushClient,
12    PushWriter, PushedFrame, SubscriptionStream, TcpRemoteTransport,
13};
14#[cfg(feature = "std")]
15pub use websocket::{
16    WebSocketDeliveredMessage, WebSocketRemoteTransport, WebSocketSubscriptionStream,
17};
18
19pub use config::{SdkConfig, build_channel_handle, build_conversation_handle};
20pub use handles::{
21    RemoteChannelHandle, RemoteConversationHandle, RemoteParticipantHandle, SdkChannelHandle,
22    SdkConversationHandle,
23};
24pub use participant::{
25    ParticipantResponseProvenance, ParticipantResumeStore, RemoteDetachReplayOutcome,
26    RemoteExpectedOperationRecovery, RemoteLostOperationResolution, RemoteLostReconnectResolution,
27    RemoteOperationRecordOutcome, RemoteOperationTransportFate, RemoteParticipantError,
28    RemoteParticipantInbound, RemoteParticipantOperation, RemoteParticipantSendOutcome,
29    RemoteReconnectAttemptOutcome, RemoteReconnectPermit, RemoteReconnectPermitOutcome,
30    RemoteReconnectPermitRecovery, RemoteReplayApplyOutcome, RemoteTransportLossOutcome,
31};
32
33#[cfg(test)]
34mod tests;
35
36use alloc::string::{String, ToString};
37use alloc::sync::Arc;
38
39use crate::connection::ConnectionPoolConfig;
40use crate::{ConversationId, SdkError};
41
42use self::protocol::{ProtocolRemoteTransport, RemoteTransport};
43
44/// Application-level address for a remote liminal server.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct ServerAddress(String);
47
48impl ServerAddress {
49    /// Creates and validates a remote server address.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`SdkError`] when the supplied address is empty.
54    pub fn new(value: impl Into<String>) -> Result<Self, SdkError> {
55        let value = value.into();
56        if value.trim().is_empty() {
57            return Err(connection_error("remote mode requires a server address"));
58        }
59        Ok(Self(value))
60    }
61
62    /// Returns the server address string.
63    #[must_use]
64    pub fn as_str(&self) -> &str {
65        self.0.as_str()
66    }
67}
68
69/// Configuration for remote SDK handles.
70#[derive(Clone, Debug)]
71pub struct RemoteConfig {
72    /// Remote server address. Remote mode cannot be created without this value.
73    pub server_address: ServerAddress,
74    /// Application-visible channel name.
75    pub channel_name: String,
76    /// Application-visible conversation identifier.
77    pub conversation_id: ConversationId,
78    /// Caller/runtime-supplied connection pool configuration.
79    pub pool_config: ConnectionPoolConfig,
80    transport: Arc<dyn RemoteTransport>,
81    /// The concretely typed WebSocket transport, retained when
82    /// [`connect_websocket`](Self::connect_websocket) installed it so callers
83    /// can drive its typed reconnect path.
84    #[cfg(feature = "std")]
85    websocket: Option<Arc<websocket::WebSocketRemoteTransport>>,
86}
87
88impl RemoteConfig {
89    /// Creates remote configuration with a required server address and pool config.
90    ///
91    /// # Errors
92    ///
93    /// Returns [`SdkError`] if the address or pool configuration is invalid.
94    pub fn new(
95        server_address: impl Into<String>,
96        channel_name: impl Into<String>,
97        conversation_id: impl Into<ConversationId>,
98        pool_config: ConnectionPoolConfig,
99    ) -> Result<Self, SdkError> {
100        Ok(Self {
101            server_address: ServerAddress::new(server_address)?,
102            channel_name: channel_name.into(),
103            conversation_id: conversation_id.into(),
104            pool_config: pool_config.validate()?,
105            transport: Arc::new(ProtocolRemoteTransport),
106            #[cfg(feature = "std")]
107            websocket: None,
108        })
109    }
110
111    /// Opens a real TCP connection to the configured server and installs the
112    /// live wire transport, replacing the in-process protocol transport.
113    ///
114    /// This performs the protocol handshake (`Connect` -> `ConnectAck`) eagerly,
115    /// so a returned configuration is already connected to the server. Subsequent
116    /// publish, subscribe, and conversation calls traverse the socket.
117    ///
118    /// # Errors
119    ///
120    /// Returns [`SdkError::Connection`] when the TCP connection cannot be
121    /// established and [`SdkError::Protocol`] when the handshake is rejected.
122    #[cfg(feature = "std")]
123    pub fn connect_tcp(mut self) -> Result<Self, SdkError> {
124        let transport = self::tcp::TcpRemoteTransport::connect(&self.server_address)?;
125        self.transport = Arc::new(transport);
126        self.websocket = None;
127        Ok(self)
128    }
129
130    /// Opens a real TCP connection whose handshake carries `auth_token`, for a
131    /// server gated by an `[auth]` section, and installs the live wire transport.
132    ///
133    /// Additive to [`connect_tcp`]: an empty token behaves identically to it. The
134    /// server compares the token during the handshake and closes the connection on
135    /// a mismatch, which surfaces here as [`SdkError::Connection`].
136    ///
137    /// # Errors
138    ///
139    /// Returns [`SdkError::Connection`] when the TCP connection cannot be
140    /// established or the token is rejected, and [`SdkError::Protocol`] when the
141    /// handshake frames cannot be encoded or sent.
142    ///
143    /// [`connect_tcp`]: Self::connect_tcp
144    #[cfg(feature = "std")]
145    pub fn connect_tcp_with_auth(mut self, auth_token: &[u8]) -> Result<Self, SdkError> {
146        let transport =
147            self::tcp::TcpRemoteTransport::connect_with_auth(&self.server_address, auth_token)?;
148        self.transport = Arc::new(transport);
149        self.websocket = None;
150        Ok(self)
151    }
152
153    /// Opens a real WebSocket connection to the configured `ws://` server
154    /// address and installs the live wire transport, replacing the in-process
155    /// protocol transport.
156    ///
157    /// This performs the WebSocket upgrade and the protocol handshake
158    /// (`Connect` -> `ConnectAck`) eagerly through the client unit's typed
159    /// permit path, so a returned configuration is already connected.
160    /// Subsequent publish, subscribe, and conversation calls traverse the
161    /// socket; the concretely typed transport stays reachable through
162    /// [`websocket_transport`](Self::websocket_transport) for the typed
163    /// reconnect path.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`SdkError::Connection`] when the address is not a usable
168    /// `ws://` URL, the connection cannot be established, or the handshake is
169    /// rejected, and [`SdkError::Protocol`] when frames cannot be encoded.
170    #[cfg(feature = "std")]
171    pub fn connect_websocket(self) -> Result<Self, SdkError> {
172        self.connect_websocket_with_auth(&[])
173    }
174
175    /// Opens a real WebSocket connection whose handshake carries
176    /// `auth_token`, for a server gated by an `[auth]` section, and installs
177    /// the live wire transport. Additive to [`connect_websocket`]: an empty
178    /// token behaves identically to it.
179    ///
180    /// # Errors
181    ///
182    /// Returns [`SdkError::Connection`] when the connection cannot be
183    /// established or the token is rejected, and [`SdkError::Protocol`] when
184    /// the handshake frames cannot be encoded or sent.
185    ///
186    /// [`connect_websocket`]: Self::connect_websocket
187    #[cfg(feature = "std")]
188    pub fn connect_websocket_with_auth(mut self, auth_token: &[u8]) -> Result<Self, SdkError> {
189        let transport = Arc::new(websocket::WebSocketRemoteTransport::connect_with_auth(
190            &self.server_address,
191            auth_token,
192        )?);
193        self.transport = Arc::clone(&transport) as Arc<dyn RemoteTransport>;
194        self.websocket = Some(transport);
195        Ok(self)
196    }
197
198    /// The concretely typed WebSocket transport installed by
199    /// [`connect_websocket`](Self::connect_websocket), when one is installed.
200    #[cfg(feature = "std")]
201    #[must_use]
202    pub fn websocket_transport(&self) -> Option<Arc<websocket::WebSocketRemoteTransport>> {
203        self.websocket.clone()
204    }
205}
206
207fn connection_error(description: &str) -> SdkError {
208    SdkError::Connection {
209        description: description.to_string(),
210    }
211}