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}