Skip to main content

liminal_sdk/
remote.rs

1mod config;
2mod handles;
3mod protocol;
4#[cfg(feature = "std")]
5mod tcp;
6
7#[cfg(feature = "std")]
8pub use tcp::{OBSERVABILITY_CHANNEL, PushClient, PushWriter, PushedFrame, TcpRemoteTransport};
9
10pub use config::{SdkConfig, build_channel_handle, build_conversation_handle};
11pub use handles::{
12    RemoteChannelHandle, RemoteConversationHandle, SdkChannelHandle, SdkConversationHandle,
13};
14
15#[cfg(test)]
16mod tests;
17
18use alloc::string::{String, ToString};
19use alloc::sync::Arc;
20use core::time::Duration;
21
22use crate::connection::{ConnectionPoolConfig, ReconnectConfig, ReconnectJitter};
23use crate::{ConversationId, SdkError};
24
25use self::protocol::{ProtocolRemoteTransport, RemoteTransport};
26
27/// Application-level address for a remote liminal server.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ServerAddress(String);
30
31impl ServerAddress {
32    /// Creates and validates a remote server address.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`SdkError`] when the supplied address is empty.
37    pub fn new(value: impl Into<String>) -> Result<Self, SdkError> {
38        let value = value.into();
39        if value.trim().is_empty() {
40            return Err(connection_error("remote mode requires a server address"));
41        }
42        Ok(Self(value))
43    }
44
45    /// Returns the server address string.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        self.0.as_str()
49    }
50}
51
52/// Configuration for remote SDK handles.
53#[derive(Clone, Debug)]
54pub struct RemoteConfig {
55    /// Remote server address. Remote mode cannot be created without this value.
56    pub server_address: ServerAddress,
57    /// Application-visible channel name.
58    pub channel_name: String,
59    /// Application-visible conversation identifier.
60    pub conversation_id: ConversationId,
61    /// Caller/runtime-supplied connection pool configuration.
62    pub pool_config: ConnectionPoolConfig,
63    /// Reconnect policy used by the SDK-003 lifecycle state machine.
64    pub reconnect_config: ReconnectConfig,
65    transport: Arc<dyn RemoteTransport>,
66}
67
68impl RemoteConfig {
69    /// Creates remote configuration with a required server address and pool config.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`SdkError`] if the address or pool configuration is invalid.
74    pub fn new(
75        server_address: impl Into<String>,
76        channel_name: impl Into<String>,
77        conversation_id: impl Into<ConversationId>,
78        pool_config: ConnectionPoolConfig,
79    ) -> Result<Self, SdkError> {
80        Ok(Self {
81            server_address: ServerAddress::new(server_address)?,
82            channel_name: channel_name.into(),
83            conversation_id: conversation_id.into(),
84            pool_config: pool_config.validate()?,
85            reconnect_config: ReconnectConfig::default(),
86            transport: Arc::new(ProtocolRemoteTransport),
87        })
88    }
89
90    /// Replaces the reconnect configuration used by remote handles.
91    #[must_use]
92    pub const fn with_reconnect_config(mut self, reconnect_config: ReconnectConfig) -> Self {
93        self.reconnect_config = reconnect_config;
94        self
95    }
96
97    /// Opens a real TCP connection to the configured server and installs the
98    /// live wire transport, replacing the in-process protocol transport.
99    ///
100    /// This performs the protocol handshake (`Connect` -> `ConnectAck`) eagerly,
101    /// so a returned configuration is already connected to the server. Subsequent
102    /// publish, subscribe, and conversation calls traverse the socket.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`SdkError::Connection`] when the TCP connection cannot be
107    /// established and [`SdkError::Protocol`] when the handshake is rejected.
108    #[cfg(feature = "std")]
109    pub fn connect_tcp(mut self) -> Result<Self, SdkError> {
110        let transport = self::tcp::TcpRemoteTransport::connect(&self.server_address)?;
111        self.transport = Arc::new(transport);
112        Ok(self)
113    }
114}
115
116/// Deterministic jitter source for lifecycle integration tests and explicit reconnect calls.
117#[derive(Clone, Copy, Debug, Default)]
118pub struct NoJitter;
119
120impl ReconnectJitter for NoJitter {
121    fn jitter(&mut self, attempt: u32, capped_delay: Duration) -> Duration {
122        core::hint::black_box((attempt, capped_delay));
123        Duration::ZERO
124    }
125}
126
127fn connection_error(description: &str) -> SdkError {
128    SdkError::Connection {
129        description: description.to_string(),
130    }
131}