Skip to main content

sim_lib_server/
connection.rs

1use std::{
2    sync::{Arc, Mutex},
3    time::Duration,
4};
5
6use sim_citizen_derive::non_citizen;
7use sim_kernel::{
8    CapabilityName, ClassRef, Consistency, Cx, Error, EvalFabric, EvalMode, EvalReply, EvalRequest,
9    Expr, Object, Result, Symbol, Value, eval_remote_capability,
10};
11
12use crate::{
13    EvalSite, FrameKind, FrameRouter, IsolationPolicy, ServerAddress, ServerFrame,
14    eval_reply_from_frame, server_frame_from_request, symbol_list_value,
15};
16
17/// State of a single server connection session.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct Session {
20    /// Session id, unique within the owning runtime.
21    pub id: u64,
22    /// Codec negotiated for this session's frames.
23    pub negotiated_codec: Symbol,
24    /// Isolation policy applied to evaluation in this session.
25    pub isolation: IsolationPolicy,
26    /// Whether the session has been closed.
27    pub closed: bool,
28}
29
30impl Session {
31    pub(crate) fn as_value(&self, cx: &mut Cx) -> Result<Value> {
32        let isolation = self.isolation.as_value(cx)?;
33        cx.factory().table(vec![
34            (Symbol::new("id"), cx.factory().string(self.id.to_string())?),
35            (
36                Symbol::new("negotiated-codec"),
37                cx.factory().symbol(self.negotiated_codec.clone())?,
38            ),
39            (Symbol::new("isolation"), isolation),
40            (Symbol::new("closed"), cx.factory().bool(self.closed)?),
41        ])
42    }
43}
44
45#[derive(Clone)]
46#[non_citizen(
47    reason = "live server connection handle; reconstruct via server/Address descriptor and connect ops",
48    kind = "handle",
49    descriptor = "server/Address"
50)]
51/// Live client handle to a server endpoint, evaluating expressions over a transport [`EvalSite`].
52pub struct Connection {
53    address: ServerAddress,
54    default_codec: Symbol,
55    supported_codecs: Vec<Symbol>,
56    site: Arc<dyn EvalSite>,
57    session: Arc<Mutex<Session>>,
58    router: Arc<FrameRouter>,
59    role: Option<Symbol>,
60}
61
62impl Connection {
63    /// Opens a connection with default role and isolation policy.
64    pub fn new(
65        address: ServerAddress,
66        default_codec: Symbol,
67        supported_codecs: Vec<Symbol>,
68        site: Arc<dyn EvalSite>,
69    ) -> Result<Self> {
70        Self::with_session(
71            address,
72            default_codec,
73            supported_codecs,
74            site,
75            None,
76            IsolationPolicy::default(),
77        )
78    }
79
80    /// Opens a connection with an explicit `role` and `isolation` policy, verifying the
81    /// address transport is available before constructing the initial session.
82    pub fn with_session(
83        address: ServerAddress,
84        default_codec: Symbol,
85        supported_codecs: Vec<Symbol>,
86        site: Arc<dyn EvalSite>,
87        role: Option<Symbol>,
88        isolation: IsolationPolicy,
89    ) -> Result<Self> {
90        address.ensure_transport_available()?;
91        Ok(Self {
92            address,
93            default_codec: default_codec.clone(),
94            supported_codecs,
95            site,
96            session: Arc::new(Mutex::new(Session {
97                id: 0,
98                negotiated_codec: default_codec,
99                isolation,
100                closed: false,
101            })),
102            router: Arc::new(FrameRouter::default()),
103            role,
104        })
105    }
106
107    /// Returns the remote address this connection targets.
108    pub fn address(&self) -> &ServerAddress {
109        &self.address
110    }
111
112    /// Returns the codec used by default for outbound frames.
113    pub fn default_codec(&self) -> &Symbol {
114        &self.default_codec
115    }
116
117    /// Returns the codecs this connection is willing to negotiate.
118    pub fn supported_codecs(&self) -> &[Symbol] {
119        &self.supported_codecs
120    }
121
122    /// Returns the underlying eval site that carries frames.
123    pub fn site(&self) -> &Arc<dyn EvalSite> {
124        &self.site
125    }
126
127    /// Returns a snapshot of the current session, or a closed placeholder if the lock is poisoned.
128    pub fn session(&self) -> Session {
129        self.session
130            .lock()
131            .map(|session| session.clone())
132            .unwrap_or(Session {
133                id: 0,
134                negotiated_codec: self.default_codec.clone(),
135                isolation: IsolationPolicy::default(),
136                closed: true,
137            })
138    }
139
140    /// Returns the role symbol attached to outbound frames, if any.
141    pub fn role(&self) -> Option<&Symbol> {
142        self.role.as_ref()
143    }
144
145    /// Evaluates `expr` remotely and returns its result value, using the connection's
146    /// default consistency.
147    pub fn request(
148        &self,
149        cx: &mut Cx,
150        expr: Expr,
151        timeout: Option<Duration>,
152        required_capabilities: Vec<CapabilityName>,
153    ) -> Result<Value> {
154        self.request_with_consistency(
155            cx,
156            expr,
157            timeout,
158            required_capabilities,
159            self.default_consistency(),
160        )
161    }
162
163    pub(crate) fn request_with_consistency(
164        &self,
165        cx: &mut Cx,
166        expr: Expr,
167        timeout: Option<Duration>,
168        required_capabilities: Vec<CapabilityName>,
169        consistency: Consistency,
170    ) -> Result<Value> {
171        self.enforce_consistency(consistency)?;
172        let request = EvalRequest {
173            expr,
174            result_shape: None,
175            required_capabilities,
176            deadline: timeout,
177            consistency,
178            mode: EvalMode::Eval,
179            answer_limit: None,
180            stream_buffer: None,
181            stream: false,
182            trace: false,
183        };
184        let reply = self.realize(cx, request)?;
185        Ok(reply.value)
186    }
187
188    /// Sends `expr` as a request frame without awaiting a synchronous result, returning the
189    /// assigned message id for later correlation. Uses the connection's default consistency.
190    pub fn send(
191        &self,
192        cx: &mut Cx,
193        expr: Expr,
194        codec: Symbol,
195        deadline: Option<Duration>,
196        required_capabilities: Vec<CapabilityName>,
197        reply_codec_hint: Option<Symbol>,
198    ) -> Result<u64> {
199        self.send_with_consistency(
200            cx,
201            expr,
202            codec,
203            deadline,
204            required_capabilities,
205            reply_codec_hint,
206            self.default_consistency(),
207        )
208    }
209
210    #[allow(clippy::too_many_arguments)]
211    pub(crate) fn send_with_consistency(
212        &self,
213        cx: &mut Cx,
214        expr: Expr,
215        codec: Symbol,
216        deadline: Option<Duration>,
217        required_capabilities: Vec<CapabilityName>,
218        reply_codec_hint: Option<Symbol>,
219        consistency: Consistency,
220    ) -> Result<u64> {
221        self.enforce_consistency(consistency)?;
222        self.enforce_remote_capability(cx)?;
223        let msg_id = self.router.fresh_msg_id();
224        let mut frame = server_frame_from_request(
225            cx,
226            &codec,
227            EvalRequest {
228                expr,
229                result_shape: None,
230                required_capabilities,
231                deadline,
232                consistency,
233                mode: EvalMode::Eval,
234                answer_limit: None,
235                stream_buffer: None,
236                stream: false,
237                trace: false,
238            },
239        )?;
240        frame.kind = FrameKind::Request;
241        frame.envelope.reply_codec_hint = reply_codec_hint;
242        frame.envelope.role = self.role.clone();
243        frame.msg_id = Some(msg_id);
244
245        let mut reply = self.site.answer(cx, frame)?;
246        if reply.correlate.is_none() {
247            reply.correlate = Some(msg_id);
248        }
249        self.router.push_inbound(reply)?;
250        Ok(msg_id)
251    }
252
253    /// Sends `expr` as a one-way notify frame, expecting no correlated reply.
254    pub fn notify(
255        &self,
256        cx: &mut Cx,
257        expr: Expr,
258        codec: Symbol,
259        deadline: Option<Duration>,
260        required_capabilities: Vec<CapabilityName>,
261    ) -> Result<()> {
262        self.enforce_remote_capability(cx)?;
263        let mut frame = ServerFrame::from_expr(
264            cx,
265            codec,
266            FrameKind::Notify,
267            &expr,
268            self.default_consistency(),
269            required_capabilities,
270            false,
271        )?;
272        frame.envelope.deadline = deadline;
273        frame.envelope.role = self.role.clone();
274        let _ = self.site.answer(cx, frame)?;
275        Ok(())
276    }
277
278    /// Pops the next buffered inbound frame, if one is queued.
279    ///
280    /// Returns [`Error::PoisonedLock`] when the router's queue mutex is
281    /// poisoned, rather than panicking.
282    pub fn receive(&self, _timeout: Option<Duration>) -> Result<Option<ServerFrame>> {
283        self.router.pop_inbound()
284    }
285
286    /// Closes the connection and marks its session closed.
287    pub fn close(&self, cx: &mut Cx) -> Result<()> {
288        self.site.close_connection(cx)?;
289        if let Ok(mut session) = self.session.lock() {
290            session.closed = true;
291        }
292        Ok(())
293    }
294}
295
296impl Object for Connection {
297    fn display(&self, _cx: &mut Cx) -> Result<String> {
298        Ok("#<server-connection>".to_owned())
299    }
300
301    fn as_any(&self) -> &dyn std::any::Any {
302        self
303    }
304}
305
306impl sim_kernel::ObjectCompat for Connection {
307    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
308        cx.factory().class_stub(
309            sim_kernel::ClassId(0),
310            Symbol::qualified("server", "Connection"),
311        )
312    }
313    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
314        self.as_table(cx)?.object().as_expr(cx)
315    }
316    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
317        let address = self.address.as_value(cx)?;
318        let default_codec = cx.factory().symbol(self.default_codec.clone())?;
319        let supported_codecs = symbol_list_value(cx, &self.supported_codecs)?;
320        let site_kind = cx.factory().string(self.site.site_kind().to_owned())?;
321        let site_address = self.site.address().as_value(cx)?;
322        let site_codecs = symbol_list_value(cx, self.site.codecs())?;
323        let session = self
324            .session
325            .lock()
326            .map_err(|_| Error::HostError("connection session mutex poisoned".to_owned()))?
327            .as_value(cx)?;
328        let next_msg_id = cx
329            .factory()
330            .string(self.router.peek_next_msg_id().to_string())?;
331        let role = match &self.role {
332            Some(role) => cx.factory().symbol(role.clone())?,
333            None => cx.factory().nil()?,
334        };
335        cx.factory().table(vec![
336            (
337                Symbol::new("kind"),
338                cx.factory().symbol(Symbol::new("connection"))?,
339            ),
340            (Symbol::new("address"), address),
341            (Symbol::new("default-codec"), default_codec),
342            (Symbol::new("supported-codecs"), supported_codecs),
343            (Symbol::new("site-kind"), site_kind),
344            (Symbol::new("site-address"), site_address),
345            (Symbol::new("site-codecs"), site_codecs),
346            (Symbol::new("session"), session),
347            (Symbol::new("role"), role),
348            (Symbol::new("next-msg-id"), next_msg_id),
349        ])
350    }
351    fn as_eval_fabric(&self) -> Option<&dyn EvalFabric> {
352        Some(self)
353    }
354}
355
356impl EvalFabric for Connection {
357    fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
358        self.enforce_remote_capability(cx)?;
359        let mut frame = server_frame_from_request(cx, self.default_codec(), request)?;
360        frame.msg_id = Some(self.router.fresh_msg_id());
361        frame.envelope.role = self.role.clone();
362        let reply = self
363            .site
364            .answer_with_timeout(cx, frame.clone(), frame.envelope.deadline)?;
365        eval_reply_from_frame(cx, &reply)
366    }
367}
368
369impl Connection {
370    fn requires_remote_capability(&self) -> bool {
371        self.address.is_remote_like() || self.site.address().is_remote_like()
372    }
373
374    pub(crate) fn default_consistency(&self) -> Consistency {
375        if self.requires_remote_capability() {
376            Consistency::RemoteOnly
377        } else {
378            Consistency::LocalFirst
379        }
380    }
381
382    fn enforce_remote_capability(&self, cx: &mut Cx) -> Result<()> {
383        if self.requires_remote_capability() {
384            cx.require(&eval_remote_capability())?;
385        }
386        Ok(())
387    }
388
389    pub(crate) fn enforce_consistency(&self, consistency: Consistency) -> Result<()> {
390        match consistency {
391            Consistency::LocalOnly if self.requires_remote_capability() => {
392                Err(Error::Eval(format!(
393                    "consistency local-only refuses remote address kind {}",
394                    self.address.kind_symbol()
395                )))
396            }
397            Consistency::RemoteOnly if !self.requires_remote_capability() => {
398                Err(Error::Eval(format!(
399                    "consistency remote-only refuses local address kind {}",
400                    self.address.kind_symbol()
401                )))
402            }
403            _ => Ok(()),
404        }
405    }
406}