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}