Skip to main content

sim_lib_server/frame/
model.rs

1use std::time::Duration;
2
3use sim_codec::DecodeLimits;
4use sim_kernel::{
5    CapabilityName, ClassRef, Consistency, Cx, EncodeOptions, Expr, Object, ReadPolicy, Result,
6    Symbol, Value,
7};
8
9use crate::codecio::{decode_frame_payload, encode_frame_payload};
10use crate::helpers::format_duration;
11
12const SERVER_FRAME_VERSION: u16 = 1;
13
14/// Discriminates the role a [`ServerFrame`] plays in the wire protocol.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FrameKind {
17    /// An eval/agent request.
18    Request,
19    /// A successful response to a request.
20    Response,
21    /// An error response to a request.
22    Error,
23    /// A one-way notification with no reply expected.
24    Notify,
25    /// The opening frame of a streamed response.
26    StreamStart,
27    /// One chunk of a streamed response.
28    StreamChunk,
29    /// The closing frame of a streamed response.
30    StreamEnd,
31    /// A codec negotiation offer.
32    Negotiate {
33        /// Codecs the sender is willing to use.
34        codecs: Vec<Symbol>,
35    },
36    /// A liveness probe.
37    Ping,
38    /// A reply to a [`FrameKind::Ping`].
39    Pong,
40    /// A lifecycle control message.
41    Lifecycle {
42        /// Lifecycle command being issued.
43        command: LifecycleCommand,
44    },
45    /// A scheduled or event-driven trigger.
46    Trigger {
47        /// Symbol naming the trigger source.
48        source: Symbol,
49        /// Trigger time in milliseconds.
50        when_ms: u64,
51    },
52    /// A role assignment for multi-hop routing.
53    Role {
54        /// Symbol naming the assigned role.
55        role: Symbol,
56        /// Hop count at which the role applies.
57        hop: u32,
58    },
59}
60
61impl FrameKind {
62    /// Returns the symbol naming this frame kind (e.g. `request`, `stream-end`).
63    pub fn as_symbol(&self) -> Symbol {
64        Symbol::new(match self {
65            Self::Request => "request",
66            Self::Response => "response",
67            Self::Error => "error",
68            Self::Notify => "notify",
69            Self::StreamStart => "stream-start",
70            Self::StreamChunk => "stream-chunk",
71            Self::StreamEnd => "stream-end",
72            Self::Negotiate { .. } => "negotiate",
73            Self::Ping => "ping",
74            Self::Pong => "pong",
75            Self::Lifecycle { .. } => "lifecycle",
76            Self::Trigger { .. } => "trigger",
77            Self::Role { .. } => "role",
78        })
79    }
80}
81
82/// Command carried by a [`FrameKind::Lifecycle`] frame.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum LifecycleCommand {
85    /// Start the server.
86    Start,
87    /// Stop the server.
88    Stop,
89    /// Suspend serving without tearing down.
90    Suspend,
91    /// Resume a suspended server.
92    Resume,
93    /// Request a health report.
94    Health,
95}
96
97/// Out-of-band metadata attached to a [`ServerFrame`] alongside its payload.
98#[derive(Clone, Debug, Default, PartialEq, Eq)]
99pub struct FrameEnvelope {
100    /// Optional deadline for handling the frame.
101    pub deadline: Option<Duration>,
102    /// Consistency policy requested for the call.
103    pub consistency: Consistency,
104    /// Whether an execution trace is requested.
105    pub trace: bool,
106    /// Capabilities the call requires.
107    pub required_capabilities: Vec<CapabilityName>,
108    /// Codec the sender prefers for the reply, if any.
109    pub reply_codec_hint: Option<Symbol>,
110    /// Routing role assigned to the frame, if any.
111    pub role: Option<Symbol>,
112    /// Hop count for multi-hop routing.
113    pub hop: u32,
114    /// Symbol naming the trigger source, if the frame was triggered.
115    pub trigger_source: Option<Symbol>,
116}
117
118impl FrameEnvelope {
119    fn as_value(&self, cx: &mut Cx) -> Result<Value> {
120        let deadline = match self.deadline {
121            Some(deadline) => cx.factory().string(format_duration(deadline))?,
122            None => cx.factory().nil()?,
123        };
124        let required_capabilities = cx.factory().list(
125            self.required_capabilities
126                .iter()
127                .map(|capability| cx.factory().string(capability.as_str().to_owned()))
128                .collect::<Result<Vec<_>>>()?,
129        )?;
130        let reply_codec_hint = match &self.reply_codec_hint {
131            Some(codec) => cx.factory().symbol(codec.clone())?,
132            None => cx.factory().nil()?,
133        };
134        let role = match &self.role {
135            Some(role) => cx.factory().symbol(role.clone())?,
136            None => cx.factory().nil()?,
137        };
138        let trigger_source = match &self.trigger_source {
139            Some(source) => cx.factory().symbol(source.clone())?,
140            None => cx.factory().nil()?,
141        };
142        cx.factory().table(vec![
143            (Symbol::new("deadline"), deadline),
144            (
145                Symbol::new("consistency"),
146                cx.factory().symbol(self.consistency.as_symbol())?,
147            ),
148            (Symbol::new("trace"), cx.factory().bool(self.trace)?),
149            (Symbol::new("requires"), required_capabilities),
150            (Symbol::new("reply-codec-hint"), reply_codec_hint),
151            (Symbol::new("role"), role),
152            (
153                Symbol::new("hop"),
154                cx.factory().string(self.hop.to_string())?,
155            ),
156            (Symbol::new("trigger-source"), trigger_source),
157        ])
158    }
159}
160
161#[sim_citizen_derive::non_citizen(
162    reason = "server frame runtime shell; class-backed descriptor is server/Frame",
163    kind = "marker",
164    descriptor = "server/Frame"
165)]
166/// A wire frame carrying a codec-encoded payload plus protocol metadata.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct ServerFrame {
169    /// Server frame format version.
170    pub version: u16,
171    /// Codec for encoding the payload.
172    pub codec: Symbol,
173    /// Identifier of this message, if assigned.
174    pub msg_id: Option<u64>,
175    /// Message id this frame correlates to, if it is a reply.
176    pub correlate: Option<u64>,
177    /// Role this frame plays in the protocol.
178    pub kind: FrameKind,
179    /// Out-of-band envelope metadata.
180    pub envelope: FrameEnvelope,
181    /// Codec-encoded payload bytes.
182    pub payload: Vec<u8>,
183}
184
185impl ServerFrame {
186    /// Builds a frame at the current version with no message or correlation id.
187    pub fn new(codec: Symbol, kind: FrameKind, envelope: FrameEnvelope, payload: Vec<u8>) -> Self {
188        Self {
189            version: SERVER_FRAME_VERSION,
190            codec,
191            msg_id: None,
192            correlate: None,
193            kind,
194            envelope,
195            payload,
196        }
197    }
198
199    /// Builds a frame by encoding `expr` under `codec` into the payload.
200    ///
201    /// Seeds the envelope with the given consistency, required capabilities,
202    /// and trace flag, leaving the remaining envelope fields at their defaults.
203    pub fn from_expr(
204        cx: &mut Cx,
205        codec: Symbol,
206        kind: FrameKind,
207        expr: &Expr,
208        consistency: Consistency,
209        required_capabilities: Vec<CapabilityName>,
210        trace: bool,
211    ) -> Result<Self> {
212        let payload = encode_frame_payload(cx, &codec, expr, EncodeOptions::default())?;
213        Ok(Self::new(
214            codec,
215            kind,
216            FrameEnvelope {
217                consistency,
218                required_capabilities,
219                trace,
220                ..FrameEnvelope::default()
221            },
222            payload,
223        ))
224    }
225
226    /// Decodes the payload back into an expression using the frame's codec.
227    pub fn decode_expr(&self, cx: &mut Cx, read_policy: ReadPolicy) -> Result<Expr> {
228        decode_frame_payload(
229            cx,
230            &self.codec,
231            &self.payload,
232            read_policy,
233            DecodeLimits::default(),
234        )
235    }
236}
237
238impl Object for ServerFrame {
239    fn display(&self, _cx: &mut Cx) -> Result<String> {
240        Ok(format!("#<server-frame {}>", self.kind.as_symbol()))
241    }
242
243    fn as_any(&self) -> &dyn std::any::Any {
244        self
245    }
246}
247
248impl sim_kernel::ObjectCompat for ServerFrame {
249    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
250        cx.factory().class_stub(
251            sim_kernel::ClassId(0),
252            Symbol::qualified("server", "ServerFrame"),
253        )
254    }
255    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
256        self.as_table(cx)?.object().as_expr(cx)
257    }
258    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
259        let msg_id = match self.msg_id {
260            Some(msg_id) => cx.factory().string(msg_id.to_string())?,
261            None => cx.factory().nil()?,
262        };
263        let correlate = match self.correlate {
264            Some(correlate) => cx.factory().string(correlate.to_string())?,
265            None => cx.factory().nil()?,
266        };
267        let envelope = self.envelope.as_value(cx)?;
268        cx.factory().table(vec![
269            (
270                Symbol::new("version"),
271                cx.factory().string(self.version.to_string())?,
272            ),
273            (
274                Symbol::new("codec"),
275                cx.factory().symbol(self.codec.clone())?,
276            ),
277            (Symbol::new("msg-id"), msg_id),
278            (Symbol::new("correlate"), correlate),
279            (
280                Symbol::new("kind"),
281                cx.factory().symbol(self.kind.as_symbol())?,
282            ),
283            (Symbol::new("envelope"), envelope),
284            (
285                Symbol::new("payload"),
286                cx.factory().bytes(self.payload.clone())?,
287            ),
288        ])
289    }
290}