Skip to main content

aimdb_core/session/
connector.rs

1//! Generic, transport-agnostic session connectors — the reusable spine every
2//! transport crate (`aimdb-uds-connector`, and later serial/TCP) wraps.
3//!
4//! A transport contributes only a [`Dialer`]/[`Listener`]/[`Connection`] triple
5//! and an [`EnvelopeCodec`]; the engine wiring (reconnect, pumps, accept loop,
6//! fan-out) is inherited here, so a new transport is a thin crate and swapping
7//! one never ripples into record/link code.
8//!
9//! - [`SessionClientConnector`] — the dialing half: on `build` it opens
10//!   [`run_client`] over the injected dialer/codec and drives [`pump_client`] for
11//!   every route under its **scheme**.
12//! - [`SessionServerConnector`] — the accepting half: it binds a [`Listener`]
13//!   (behind a factory, so bind errors surface synchronously from `build`) and
14//!   drives [`serve`] with an injected dispatch + codec.
15//!
16//! The **scheme** is a constructor argument (default `"remote"`) decoupling the
17//! logical routing key from the transport, so two transports can coexist under
18//! different schemes.
19
20use alloc::boxed::Box;
21use alloc::string::{String, ToString};
22use alloc::sync::Arc;
23use alloc::vec;
24use alloc::vec::Vec;
25use core::future::Future;
26use core::pin::Pin;
27
28use crate::builder::AimDb;
29use crate::connector::ConnectorBuilder;
30use crate::session::{
31    pump_client, run_client, serve, ClientConfig, Dialer, Dispatch, EnvelopeCodec, Listener,
32    OneShot, SessionConfig,
33};
34use crate::{DbError, DbResult};
35
36/// The default scheme a session connector registers when none is given.
37pub const DEFAULT_SCHEME: &str = "remote";
38
39type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
40type BuildFuture<'a> = Pin<Box<dyn Future<Output = DbResult<Vec<BoxFuture>>> + Send + 'a>>;
41
42// ===========================================================================
43// Client — dials a peer, mirrors records under `scheme`.
44// ===========================================================================
45
46/// Mirrors records to/from a peer reached via the dialer `D`, speaking codec `C`,
47/// under a logical [`scheme`](ConnectorBuilder::scheme). A transport crate wraps
48/// it in a one-line sugar constructor (e.g. `UdsClient`).
49pub struct SessionClientConnector<D, C> {
50    scheme: String,
51    dialer: OneShot<D>,
52    codec: C,
53    config: ClientConfig,
54}
55
56impl<D, C> SessionClientConnector<D, C> {
57    /// Mirror records over `dialer`, framing messages with `codec`. The scheme
58    /// defaults to `"remote"`.
59    pub fn new(dialer: D, codec: C) -> Self {
60        Self {
61            scheme: DEFAULT_SCHEME.to_string(),
62            dialer: OneShot::new(dialer),
63            codec,
64            config: ClientConfig::default(),
65        }
66    }
67
68    /// Override the scheme this connector registers (so `<scheme>://<record>`
69    /// links validate and route here).
70    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
71        self.scheme = scheme.into();
72        self
73    }
74
75    /// Override the client engine config (reconnect policy, keepalive, etc.).
76    pub fn with_config(mut self, config: ClientConfig) -> Self {
77        self.config = config;
78        self
79    }
80}
81
82impl<D, C> ConnectorBuilder for SessionClientConnector<D, C>
83where
84    D: Dialer + Send + 'static,
85    C: EnvelopeCodec + Clone + 'static,
86{
87    fn build<'a>(&'a self, db: &'a AimDb) -> BuildFuture<'a> {
88        Box::pin(async move {
89            // Taken on first poll, not at call time: a `build()` future dropped
90            // before it is polled must leave the dialer where it was.
91            let dialer = self
92                .dialer
93                .take()
94                .ok_or_else(|| DbError::InvalidOperation {
95                    operation: "SessionClientConnector::build".to_string(),
96                    reason: "the moved-in dialer was already taken; build() ran twice".to_string(),
97                })?;
98            let (handle, engine_fut) = run_client(
99                dialer,
100                self.codec.clone(),
101                self.config.clone(),
102                db.runtime_ops(),
103            );
104            // One pump future per route; each holds a `ClientHandle` clone, so the
105            // engine stays alive as long as any mirror runs. `handle` drops here.
106            let mut futures = pump_client(db, &self.scheme, &handle);
107            futures.push(engine_fut);
108            Ok(futures)
109        })
110    }
111
112    fn scheme(&self) -> &str {
113        &self.scheme
114    }
115}
116
117// ===========================================================================
118// Server — accepts connections, serves a dispatch under `scheme`.
119// ===========================================================================
120
121/// Accepts connections from a [`Listener`] `L` and serves them with a dispatch,
122/// speaking codec `C`, under a logical [`scheme`](ConnectorBuilder::scheme).
123///
124/// Two factories keep it transport- and protocol-agnostic:
125/// - `listener_factory` runs at `build` time and returns `DbResult<L>`, so the
126///   bind happens there and any error surfaces synchronously from `build`.
127/// - `dispatch_factory` turns the live `&AimDb` into an `Arc<dyn Dispatch>`
128///   (e.g. an `AimxDispatch`), so the spine never names a concrete protocol.
129pub struct SessionServerConnector<C, LF, DF> {
130    scheme: String,
131    listener_factory: LF,
132    codec: C,
133    dispatch_factory: DF,
134    config: SessionConfig,
135}
136
137impl<C, LF, DF> SessionServerConnector<C, LF, DF> {
138    /// Build a server connector. `listener_factory` binds the listener at
139    /// `build` time; `dispatch_factory` produces the per-server dispatch from the
140    /// live db. The scheme defaults to `"remote"`.
141    pub fn new(
142        listener_factory: LF,
143        codec: C,
144        dispatch_factory: DF,
145        config: SessionConfig,
146    ) -> Self {
147        Self {
148            scheme: DEFAULT_SCHEME.to_string(),
149            listener_factory,
150            codec,
151            dispatch_factory,
152            config,
153        }
154    }
155
156    /// Override the scheme this connector registers.
157    pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
158        self.scheme = scheme.into();
159        self
160    }
161}
162
163impl<L, C, LF, DF> ConnectorBuilder for SessionServerConnector<C, LF, DF>
164where
165    L: Listener + 'static,
166    C: EnvelopeCodec + Clone + 'static,
167    LF: Fn() -> DbResult<L> + Send + Sync + 'static,
168    DF: Fn(&AimDb) -> Arc<dyn Dispatch> + Send + Sync + 'static,
169{
170    fn build<'a>(&'a self, db: &'a AimDb) -> BuildFuture<'a> {
171        // Bind synchronously so a bind error surfaces from `build`.
172        let listener = (self.listener_factory)();
173        let dispatch = (self.dispatch_factory)(db);
174        let codec = Arc::new(self.codec.clone());
175        let config = self.config.clone();
176        Box::pin(async move {
177            let listener = listener?;
178            let fut: BoxFuture = Box::pin(serve(listener, codec, dispatch, config));
179            Ok(vec![fut])
180        })
181    }
182
183    fn scheme(&self) -> &str {
184        &self.scheme
185    }
186}