Skip to main content

liminal_sdk/remote/
handles.rs

1use alloc::string::{String, ToString};
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use spin::Mutex;
8
9use crate::connection::{
10    ConnectionEvent, ConnectionLifecycle, ConnectionPool, ConnectionState, ResumeRequest,
11    SubscriptionId,
12};
13use crate::embedded::{
14    EmbeddedChannelHandle, EmbeddedConversationHandle, EmptyLifecycleStream, ReadyResult,
15    SdkSubscription,
16};
17use crate::{
18    ChannelHandle, ConversationHandle, ConversationId, DeliveryAck, PressureResponse,
19    SchemaValidate, SdkError,
20};
21
22pub use super::participant::RemoteParticipantHandle;
23
24use super::config::SdkConfig;
25use super::protocol::{
26    RemoteTransport, WireConversationRequest, WirePublishRequest, WireResumeRequest,
27    WireSubscribeRequest, deserialize_payload,
28};
29use super::{RemoteConfig, ServerAddress, connection_error};
30
31#[derive(Debug)]
32struct RemoteChannelState {
33    lifecycle: ConnectionLifecycle,
34    pool: ConnectionPool,
35    next_subscription: u64,
36}
37
38/// Channel handle that communicates through SDK-internal wire protocol transport.
39#[derive(Clone, Debug)]
40pub struct RemoteChannelHandle {
41    server_address: ServerAddress,
42    channel_name: String,
43    state: Arc<Mutex<RemoteChannelState>>,
44    transport: Arc<dyn RemoteTransport>,
45}
46
47impl RemoteChannelHandle {
48    /// Creates a remote channel handle from validated configuration.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`SdkError`] if the connection pool cannot be created.
53    pub fn new(config: &RemoteConfig) -> Result<Self, SdkError> {
54        Ok(Self {
55            server_address: config.server_address.clone(),
56            channel_name: config.channel_name.clone(),
57            state: Arc::new(Mutex::new(RemoteChannelState {
58                lifecycle: ConnectionLifecycle::new(),
59                pool: ConnectionPool::new(config.pool_config)?,
60                next_subscription: 0,
61            })),
62            transport: Arc::clone(&config.transport),
63        })
64    }
65
66    /// Returns current lifecycle state from the SDK-003 state machine.
67    #[must_use]
68    pub fn connection_state(&self) -> ConnectionState {
69        self.state.lock().lifecycle.state().clone()
70    }
71
72    /// Records that an externally authorized real reconnect attempt started.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`SdkError`] when the lifecycle rejects the transition.
77    pub fn reconnect_started(&self) -> Result<(), SdkError> {
78        self.state.lock().lifecycle.reconnect_started()
79    }
80
81    /// Marks the remote channel connected and builds subscription resume requests.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`SdkError`] if lifecycle transition or recovery state is invalid.
86    pub fn connected(&self) -> Result<Vec<ResumeRequest>, SdkError> {
87        // Compute the resume requests under the lock, then release it before any
88        // transport I/O so concurrent callers do not spin-wait during resume calls.
89        let requests = {
90            let mut state = self.state.lock();
91            let previous = state.lifecycle.state().clone();
92            state.lifecycle.connected()?;
93            let event = ConnectionEvent::new(previous, state.lifecycle.state().clone());
94            state.pool.resume_requests_for_transition(&event)?
95        };
96        for request in &requests {
97            let wire_request = WireResumeRequest::new(*request);
98            self.transport.resume(&self.server_address, &wire_request)?;
99        }
100        Ok(requests)
101    }
102
103    /// Marks a subscription active and assigns it to the configured pool.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`SdkError`] if a subscription id overflows or pool assignment fails.
108    pub fn track_subscription(&self) -> Result<SubscriptionId, SdkError> {
109        let mut state = self.state.lock();
110        let id = SubscriptionId::new(state.next_subscription);
111        state.next_subscription =
112            state
113                .next_subscription
114                .checked_add(1)
115                .ok_or_else(|| SdkError::Store {
116                    description: "subscription id overflow".to_string(),
117                })?;
118        state.pool.assign_subscription(id)?;
119        Ok(id)
120    }
121
122    /// Records an acknowledged sequence for subscription recovery.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`SdkError`] if the subscription is not active in the pool.
127    pub fn acknowledge(
128        &self,
129        subscription_id: SubscriptionId,
130        sequence: u64,
131    ) -> Result<(), SdkError> {
132        let mut state = self.state.lock();
133        state.pool.acknowledge(subscription_id, sequence)
134    }
135
136    /// Returns remote server address used by this handle.
137    #[must_use]
138    pub const fn server_address(&self) -> &ServerAddress {
139        &self.server_address
140    }
141}
142
143impl ChannelHandle for RemoteChannelHandle {
144    type Subscription<M>
145        = SdkSubscription<M>
146    where
147        M: DeserializeOwned;
148
149    type ReplyFuture<'a, Resp>
150        = ReadyResult<Resp>
151    where
152        Self: 'a,
153        Resp: DeserializeOwned + 'a;
154
155    fn publish<M>(&self, message: M) -> Result<PressureResponse, SdkError>
156    where
157        M: Serialize + SchemaValidate,
158    {
159        let request = WirePublishRequest::new(&self.channel_name, &message)?;
160        self.transport.publish(&self.server_address, &request)
161    }
162
163    fn subscribe<M>(&self) -> Self::Subscription<M>
164    where
165        M: DeserializeOwned,
166    {
167        let outcome = self.track_subscription().and_then(|subscription_id| {
168            let connection_id = self
169                .state
170                .lock()
171                .pool
172                .connection_for_subscription(subscription_id)
173                .ok_or_else(|| connection_error("subscription was not assigned to the pool"))?;
174            let request = WireSubscribeRequest::new(
175                &self.channel_name,
176                subscription_id,
177                connection_id.get(),
178            )?;
179            self.transport.subscribe(&self.server_address, &request)
180        });
181
182        match outcome {
183            Ok(()) => SdkSubscription::empty(),
184            Err(error) => SdkSubscription::error(error),
185        }
186    }
187
188    fn request_reply<Req, Resp>(&self, request: Req) -> ReadyResult<Resp>
189    where
190        Req: Serialize + SchemaValidate,
191        Resp: DeserializeOwned,
192    {
193        ReadyResult::new(self.request_reply_blocking(&request))
194    }
195}
196
197impl RemoteChannelHandle {
198    /// Publishes a message with an idempotency key and returns a genuine delivery
199    /// ack.
200    ///
201    /// The idempotency key drives dedup-on-delivery on the server: a re-publish of
202    /// the same key is delivered to subscribers at most once. The returned
203    /// [`DeliveryAck`] reports whether this publish was genuinely accepted by a
204    /// subscriber ([`DeliveryAck::is_accepted`]), which a caller such as the aion
205    /// outbox uses to treat the send as done only on real acceptance. This is
206    /// distinct from the backpressure-only [`publish`](ChannelHandle::publish).
207    ///
208    /// # Errors
209    ///
210    /// Returns [`SdkError`] when the message cannot be serialized, the round trip
211    /// fails, or the transport cannot produce a genuine delivery ack.
212    pub fn publish_with_idempotency_key<M>(
213        &self,
214        message: &M,
215        idempotency_key: &str,
216    ) -> Result<DeliveryAck, SdkError>
217    where
218        M: Serialize + SchemaValidate,
219    {
220        let request =
221            WirePublishRequest::with_idempotency_key(&self.channel_name, message, idempotency_key)?;
222        self.transport
223            .publish_with_delivery(&self.server_address, &request)
224    }
225
226    /// Performs a correlated request-reply round trip over the transport and
227    /// deserializes the reply.
228    ///
229    /// The round trip rides the conversation request-reply path: the channel name
230    /// is used as the conversation correlation id and subject, so the reply the
231    /// server returns is matched back to this request by `conversation_id` on the
232    /// single synchronous connection. Schema validation still runs on the request
233    /// so a request-reply enforces the same typing contract as `publish`.
234    ///
235    /// # Errors
236    ///
237    /// Returns [`SdkError`] when the request cannot be serialized, the round trip
238    /// fails, or the reply cannot be deserialized into `Resp`.
239    fn request_reply_blocking<Req, Resp>(&self, request: &Req) -> Result<Resp, SdkError>
240    where
241        Req: Serialize + SchemaValidate,
242        Resp: DeserializeOwned,
243    {
244        let conversation_id = ConversationId::new(self.channel_name.clone());
245        let wire_request = WireConversationRequest::new(&conversation_id, request)?;
246        let reply = self
247            .transport
248            .request_reply_conversation(&self.server_address, &wire_request)?;
249        deserialize_payload(&reply)
250    }
251}
252
253/// Conversation handle that communicates through SDK-internal wire protocol transport.
254#[derive(Clone, Debug)]
255pub struct RemoteConversationHandle {
256    server_address: ServerAddress,
257    conversation_id: ConversationId,
258    lifecycle: Arc<Mutex<ConnectionLifecycle>>,
259    transport: Arc<dyn RemoteTransport>,
260    /// Buffered reply bytes from the most recent [`request`](Self::request) round
261    /// trip, drained by the next [`receive`](ConversationHandle::receive). The
262    /// synchronous transport completes the round trip inside `request`, so the
263    /// correlated reply is held here until the caller deserializes it.
264    pending_reply: Arc<Mutex<Option<Vec<u8>>>>,
265}
266
267impl RemoteConversationHandle {
268    /// Creates a remote conversation handle from validated configuration.
269    #[must_use]
270    pub fn new(config: &RemoteConfig) -> Self {
271        Self {
272            server_address: config.server_address.clone(),
273            conversation_id: config.conversation_id.clone(),
274            lifecycle: Arc::new(Mutex::new(ConnectionLifecycle::new())),
275            transport: Arc::clone(&config.transport),
276            pending_reply: Arc::new(Mutex::new(None)),
277        }
278    }
279
280    /// Sends a typed request on this conversation and blocks for its correlated
281    /// reply, buffering the reply for the next [`receive`](ConversationHandle::receive).
282    ///
283    /// This is the request leg of the conversation request-reply pattern. It is
284    /// kept distinct from [`send`](ConversationHandle::send), which stays
285    /// fire-and-forget (the server is silent on success): only `request` sets the
286    /// reply-requested flag and waits for the server's correlated answer. The aion
287    /// dispatch model (`send` request, then `receive` reply) maps onto a `request`
288    /// followed by `receive`.
289    ///
290    /// # Errors
291    ///
292    /// Returns [`SdkError`] when the request cannot be serialized or the round
293    /// trip fails.
294    pub fn request<Req>(&self, request: Req) -> Result<(), SdkError>
295    where
296        Req: Serialize,
297    {
298        let wire_request = WireConversationRequest::new(&self.conversation_id, &request)?;
299        let reply = self
300            .transport
301            .request_reply_conversation(&self.server_address, &wire_request)?;
302        *self.pending_reply.lock() = Some(reply);
303        Ok(())
304    }
305
306    /// Returns current lifecycle state from the SDK-003 state machine.
307    #[must_use]
308    pub fn connection_state(&self) -> ConnectionState {
309        self.lifecycle.lock().state().clone()
310    }
311
312    /// Returns remote server address used by this handle.
313    #[must_use]
314    pub const fn server_address(&self) -> &ServerAddress {
315        &self.server_address
316    }
317}
318
319impl ConversationHandle for RemoteConversationHandle {
320    type ReceiveFuture<'a, M>
321        = ReadyResult<M>
322    where
323        Self: 'a,
324        M: DeserializeOwned + 'a;
325
326    type LifecycleStream = EmptyLifecycleStream;
327
328    fn send<M>(&self, message: M) -> Result<(), SdkError>
329    where
330        M: Serialize,
331    {
332        let request = WireConversationRequest::new(&self.conversation_id, &message)?;
333        self.transport
334            .send_conversation(&self.server_address, &request)
335    }
336
337    fn receive<M>(&self) -> ReadyResult<M>
338    where
339        M: DeserializeOwned,
340    {
341        ReadyResult::new(self.receive_blocking())
342    }
343
344    fn lifecycle(&self) -> Self::LifecycleStream {
345        EmptyLifecycleStream
346    }
347}
348
349impl RemoteConversationHandle {
350    /// Drains and deserializes the correlated reply buffered by the most recent
351    /// [`request`](Self::request).
352    ///
353    /// # Errors
354    ///
355    /// Returns [`SdkError::Conversation`] when no reply is pending (no `request`
356    /// has completed since the last `receive`), or [`SdkError::Serialization`]
357    /// when the buffered reply cannot be deserialized into `M`.
358    fn receive_blocking<M>(&self) -> Result<M, SdkError>
359    where
360        M: DeserializeOwned,
361    {
362        let payload = self
363            .pending_reply
364            .lock()
365            .take()
366            .ok_or_else(|| SdkError::Conversation {
367                conversation_id: self.conversation_id.as_str().to_string(),
368                description: "no correlated reply is pending; call request before receive"
369                    .to_string(),
370            })?;
371        deserialize_payload(&payload)
372    }
373}
374
375/// Runtime-selected channel handle that keeps deployment differences behind configuration.
376#[derive(Clone, Debug)]
377pub enum SdkChannelHandle {
378    /// Embedded direct in-process handle.
379    Embedded(EmbeddedChannelHandle),
380    /// Remote protocol-backed handle.
381    Remote(RemoteChannelHandle),
382}
383
384impl SdkChannelHandle {
385    /// Creates a channel handle selected by SDK configuration.
386    ///
387    /// # Errors
388    ///
389    /// Returns [`SdkError`] if the selected handle cannot be initialized.
390    pub fn new(config: &SdkConfig) -> Result<Self, SdkError> {
391        match config {
392            SdkConfig::Embedded(config) => Ok(Self::Embedded(EmbeddedChannelHandle::new(config))),
393            SdkConfig::Remote(config) => Ok(Self::Remote(RemoteChannelHandle::new(config)?)),
394        }
395    }
396}
397
398impl ChannelHandle for SdkChannelHandle {
399    type Subscription<M>
400        = SdkSubscription<M>
401    where
402        M: DeserializeOwned;
403
404    type ReplyFuture<'a, Resp>
405        = ReadyResult<Resp>
406    where
407        Self: 'a,
408        Resp: DeserializeOwned + 'a;
409
410    fn publish<M>(&self, message: M) -> Result<PressureResponse, SdkError>
411    where
412        M: Serialize + SchemaValidate,
413    {
414        match self {
415            Self::Embedded(handle) => handle.publish(message),
416            Self::Remote(handle) => handle.publish(message),
417        }
418    }
419
420    fn subscribe<M>(&self) -> Self::Subscription<M>
421    where
422        M: DeserializeOwned,
423    {
424        match self {
425            Self::Embedded(handle) => handle.subscribe(),
426            Self::Remote(handle) => handle.subscribe(),
427        }
428    }
429
430    fn request_reply<Req, Resp>(&self, request: Req) -> ReadyResult<Resp>
431    where
432        Req: Serialize + SchemaValidate,
433        Resp: DeserializeOwned,
434    {
435        match self {
436            Self::Embedded(handle) => handle.request_reply(request),
437            Self::Remote(handle) => handle.request_reply(request),
438        }
439    }
440}
441
442/// Runtime-selected conversation handle that keeps deployment differences behind configuration.
443#[derive(Clone, Debug)]
444pub enum SdkConversationHandle {
445    /// Embedded direct in-process handle.
446    Embedded(EmbeddedConversationHandle),
447    /// Remote protocol-backed handle.
448    Remote(RemoteConversationHandle),
449}
450
451impl SdkConversationHandle {
452    /// Creates a conversation handle selected by SDK configuration.
453    ///
454    /// # Errors
455    ///
456    /// Returns [`SdkError`] if the selected handle cannot be initialized.
457    pub fn new(config: &SdkConfig) -> Result<Self, SdkError> {
458        match config {
459            SdkConfig::Embedded(config) => {
460                Ok(Self::Embedded(EmbeddedConversationHandle::new(config)))
461            }
462            SdkConfig::Remote(config) => Ok(Self::Remote(RemoteConversationHandle::new(config))),
463        }
464    }
465}
466
467impl ConversationHandle for SdkConversationHandle {
468    type ReceiveFuture<'a, M>
469        = ReadyResult<M>
470    where
471        Self: 'a,
472        M: DeserializeOwned + 'a;
473
474    type LifecycleStream = EmptyLifecycleStream;
475
476    fn send<M>(&self, message: M) -> Result<(), SdkError>
477    where
478        M: Serialize,
479    {
480        match self {
481            Self::Embedded(handle) => handle.send(message),
482            Self::Remote(handle) => handle.send(message),
483        }
484    }
485
486    fn receive<M>(&self) -> ReadyResult<M>
487    where
488        M: DeserializeOwned,
489    {
490        match self {
491            Self::Embedded(handle) => handle.receive(),
492            Self::Remote(handle) => handle.receive(),
493        }
494    }
495
496    fn lifecycle(&self) -> Self::LifecycleStream {
497        EmptyLifecycleStream
498    }
499}