Skip to main content

sim_lib_stream_fabric/
control.rs

1use sim_kernel::{CapabilityName, Error, Expr, Result, Symbol};
2use sim_lib_server::{
3    FrameEnvelope, FrameKind, ServerFrame, stream_chunk_frame_from_expr, stream_frame_to_expr,
4};
5use sim_lib_stream_core::{
6    DataPacket, StreamEnvelope, StreamFaultKind, StreamFaultSpec, StreamMetadata, StreamPacket,
7    stream_cancel_capability, stream_open_capability, stream_push_capability,
8    stream_read_capability, stream_remote_network_capability, stream_stats_capability,
9};
10use sim_value::kind::expr_kind;
11
12/// Control-plane operation exchanged over remote stream-fabric frames.
13///
14/// Each variant names one action a peer can take on a placed stream -- opening
15/// it, pulling the next chunk, pushing an envelope, ending it, or reporting a
16/// fault -- so the location-transparent eval surface can drive a remote stream
17/// without exposing transport-specific APIs. A [`StreamControl`] round-trips
18/// through the shared `Expr` graph (see [`StreamControl::to_expr`] and the
19/// `TryFrom<Expr>` impl) and is carried by [`stream_control_frame_from_control`].
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum StreamControl {
22    /// Open a stream with the given identity and [`StreamMetadata`].
23    Open {
24        /// Identifier of the stream being opened.
25        stream_id: Symbol,
26        /// Declared metadata (media, direction, clock domain, buffer policy).
27        metadata: StreamMetadata,
28    },
29    /// Request the next chunk(s), bounded by an optional element limit.
30    Next {
31        /// Identifier of the stream being pulled.
32        stream_id: Symbol,
33        /// Maximum number of elements to return, or `None` for unbounded.
34        limit: Option<u64>,
35    },
36    /// Push one stream envelope toward the peer.
37    Push {
38        /// Identifier of the stream being pushed to.
39        stream_id: Symbol,
40        /// Boxed envelope carrying the packet, ticks, and transport profile.
41        envelope: Box<StreamEnvelope>,
42    },
43    /// Close a stream cleanly once production is finished.
44    Close {
45        /// Identifier of the stream being closed.
46        stream_id: Symbol,
47    },
48    /// Cancel a stream early, abandoning any remaining production.
49    Cancel {
50        /// Identifier of the stream being cancelled.
51        stream_id: Symbol,
52    },
53    /// Request runtime statistics for a stream.
54    Stats {
55        /// Identifier of the stream being queried.
56        stream_id: Symbol,
57    },
58    /// Request the metadata table for a stream.
59    Metadata {
60        /// Identifier of the stream being queried.
61        stream_id: Symbol,
62    },
63    /// Report a fault against a stream.
64    Fault {
65        /// Identifier of the faulting stream.
66        stream_id: Symbol,
67        /// Fault kind and occurrence count.
68        fault: StreamFaultSpec,
69    },
70}
71
72impl StreamControl {
73    /// Returns the operation symbol naming this control variant.
74    pub fn operation(&self) -> Symbol {
75        match self {
76            Self::Open { .. } => stream_control_open_symbol(),
77            Self::Next { .. } => stream_control_next_symbol(),
78            Self::Push { .. } => stream_control_push_symbol(),
79            Self::Close { .. } => stream_control_close_symbol(),
80            Self::Cancel { .. } => stream_control_cancel_symbol(),
81            Self::Stats { .. } => stream_control_stats_symbol(),
82            Self::Metadata { .. } => stream_control_metadata_symbol(),
83            Self::Fault { .. } => stream_control_fault_symbol(),
84        }
85    }
86
87    /// Returns the capability a peer must hold to perform this control action.
88    pub fn required_capability(&self) -> CapabilityName {
89        match self {
90            Self::Open { .. } => stream_open_capability(),
91            Self::Next { .. } => stream_read_capability(),
92            Self::Push { .. } => stream_push_capability(),
93            Self::Close { .. } | Self::Cancel { .. } => stream_cancel_capability(),
94            Self::Stats { .. } | Self::Metadata { .. } | Self::Fault { .. } => {
95                stream_stats_capability()
96            }
97        }
98    }
99
100    /// Returns the stream identifier targeted by this control action.
101    pub fn stream_id(&self) -> &Symbol {
102        match self {
103            Self::Open { stream_id, .. }
104            | Self::Next { stream_id, .. }
105            | Self::Push { stream_id, .. }
106            | Self::Close { stream_id }
107            | Self::Cancel { stream_id }
108            | Self::Stats { stream_id }
109            | Self::Metadata { stream_id }
110            | Self::Fault { stream_id, .. } => stream_id,
111        }
112    }
113
114    /// Encodes this control action as a stream data packet `Expr`.
115    ///
116    /// The inverse is the `TryFrom<Expr>` impl, which reparses the packet back
117    /// into a [`StreamControl`].
118    pub fn to_expr(&self) -> Expr {
119        StreamPacket::data(self.operation(), self.payload_expr()).to_expr()
120    }
121
122    fn payload_expr(&self) -> Expr {
123        let mut entries = vec![
124            key_expr("control", Expr::Symbol(stream_control_tag_symbol())),
125            key_expr("stream-id", Expr::Symbol(self.stream_id().clone())),
126        ];
127        match self {
128            Self::Open { metadata, .. } => {
129                entries.push(key_expr("metadata", metadata.table_expr()));
130            }
131            Self::Next { limit, .. } => {
132                entries.push(key_expr(
133                    "limit",
134                    limit
135                        .map(|limit| Expr::String(limit.to_string()))
136                        .unwrap_or(Expr::Nil),
137                ));
138            }
139            Self::Push { envelope, .. } => {
140                entries.push(key_expr("envelope", envelope.to_expr()));
141            }
142            Self::Close { .. }
143            | Self::Cancel { .. }
144            | Self::Stats { .. }
145            | Self::Metadata { .. } => {}
146            Self::Fault { fault, .. } => {
147                entries.push(key_expr("fault", Expr::Symbol(fault.kind.symbol())));
148                entries.push(key_expr("count", Expr::String(fault.count.to_string())));
149            }
150        }
151        Expr::Map(entries)
152    }
153}
154
155impl TryFrom<Expr> for StreamControl {
156    type Error = Error;
157
158    fn try_from(expr: Expr) -> Result<Self> {
159        let StreamPacket::Data(DataPacket { kind, payload }) = StreamPacket::try_from(expr)? else {
160            return Err(Error::TypeMismatch {
161                expected: "stream fabric control data packet",
162                found: "stream packet",
163            });
164        };
165        let entries = map_entries(&payload)?;
166        let tag = symbol_field(entries, "control")?;
167        if *tag != stream_control_tag_symbol() {
168            return Err(Error::Eval(format!(
169                "unknown stream fabric control tag {}",
170                tag.as_qualified_str()
171            )));
172        }
173        let stream_id = symbol_field(entries, "stream-id")?.clone();
174        match kind.as_qualified_str().as_str() {
175            "stream/fabric/open" => {
176                ensure_fields(entries, &["control", "stream-id", "metadata"])?;
177                Ok(Self::Open {
178                    stream_id,
179                    metadata: StreamMetadata::from_table_expr(field(entries, "metadata")?)?,
180                })
181            }
182            "stream/fabric/next" => {
183                ensure_fields(entries, &["control", "stream-id", "limit"])?;
184                Ok(Self::Next {
185                    stream_id,
186                    limit: optional_u64(field(entries, "limit")?)?,
187                })
188            }
189            "stream/fabric/push" => {
190                ensure_fields(entries, &["control", "stream-id", "envelope"])?;
191                Ok(Self::Push {
192                    stream_id,
193                    envelope: Box::new(StreamEnvelope::try_from(
194                        field(entries, "envelope")?.clone(),
195                    )?),
196                })
197            }
198            "stream/fabric/close" => {
199                ensure_fields(entries, &["control", "stream-id"])?;
200                Ok(Self::Close { stream_id })
201            }
202            "stream/fabric/cancel" => {
203                ensure_fields(entries, &["control", "stream-id"])?;
204                Ok(Self::Cancel { stream_id })
205            }
206            "stream/fabric/stats" => {
207                ensure_fields(entries, &["control", "stream-id"])?;
208                Ok(Self::Stats { stream_id })
209            }
210            "stream/fabric/metadata" => {
211                ensure_fields(entries, &["control", "stream-id"])?;
212                Ok(Self::Metadata { stream_id })
213            }
214            "stream/fabric/fault" => {
215                ensure_fields(entries, &["control", "stream-id", "fault", "count"])?;
216                Ok(Self::Fault {
217                    stream_id,
218                    fault: StreamFaultSpec::new(
219                        StreamFaultKind::from_symbol(symbol_field(entries, "fault")?)?,
220                        parse_u64(field(entries, "count")?)? as usize,
221                    ),
222                })
223            }
224            other => Err(Error::Eval(format!(
225                "unknown stream fabric control operation {other}"
226            ))),
227        }
228    }
229}
230
231/// Encodes a [`StreamControl`] into a server stream-chunk frame.
232///
233/// Requires the remote-network capability and the action's own
234/// [`StreamControl::required_capability`] before encoding the control packet
235/// with `codec` into a frame.
236pub fn stream_control_frame_from_control(
237    cx: &mut sim_kernel::Cx,
238    codec: Symbol,
239    control: &StreamControl,
240    envelope: FrameEnvelope,
241) -> Result<ServerFrame> {
242    cx.require(&stream_remote_network_capability())?;
243    cx.require(&control.required_capability())?;
244    stream_chunk_frame_from_expr(cx, codec, &control.to_expr(), envelope)
245}
246
247/// Decodes a server stream-chunk frame back into a [`StreamControl`].
248///
249/// Returns an error when the frame is not a stream-chunk frame or does not
250/// decode to a control payload.
251pub fn stream_control_from_frame(
252    cx: &mut sim_kernel::Cx,
253    frame: &ServerFrame,
254) -> Result<StreamControl> {
255    if frame.kind != FrameKind::StreamChunk {
256        return Err(Error::Eval(format!(
257            "stream fabric control expected stream chunk frame, got {}",
258            frame.kind.as_symbol()
259        )));
260    }
261    let expr = stream_frame_to_expr(cx, frame)?.ok_or_else(|| {
262        Error::Eval("stream fabric control frame did not decode to a payload".to_owned())
263    })?;
264    StreamControl::try_from(expr)
265}
266
267/// Returns every stream-fabric control operation symbol, one per variant.
268pub fn stream_control_operation_symbols() -> [Symbol; 8] {
269    [
270        stream_control_open_symbol(),
271        stream_control_next_symbol(),
272        stream_control_push_symbol(),
273        stream_control_close_symbol(),
274        stream_control_cancel_symbol(),
275        stream_control_stats_symbol(),
276        stream_control_metadata_symbol(),
277        stream_control_fault_symbol(),
278    ]
279}
280
281/// Returns the capability required to perform `control`.
282///
283/// Free-function form of [`StreamControl::required_capability`].
284pub fn stream_control_required_capability(control: &StreamControl) -> CapabilityName {
285    control.required_capability()
286}
287
288/// Returns the operation symbol for the open control action.
289pub fn stream_control_open_symbol() -> Symbol {
290    Symbol::qualified("stream/fabric", "open")
291}
292
293/// Returns the operation symbol for the next control action.
294pub fn stream_control_next_symbol() -> Symbol {
295    Symbol::qualified("stream/fabric", "next")
296}
297
298/// Returns the operation symbol for the push control action.
299pub fn stream_control_push_symbol() -> Symbol {
300    Symbol::qualified("stream/fabric", "push")
301}
302
303/// Returns the operation symbol for the close control action.
304pub fn stream_control_close_symbol() -> Symbol {
305    Symbol::qualified("stream/fabric", "close")
306}
307
308/// Returns the operation symbol for the cancel control action.
309pub fn stream_control_cancel_symbol() -> Symbol {
310    Symbol::qualified("stream/fabric", "cancel")
311}
312
313/// Returns the operation symbol for the stats control action.
314pub fn stream_control_stats_symbol() -> Symbol {
315    Symbol::qualified("stream/fabric", "stats")
316}
317
318/// Returns the operation symbol for the metadata control action.
319pub fn stream_control_metadata_symbol() -> Symbol {
320    Symbol::qualified("stream/fabric", "metadata")
321}
322
323/// Returns the operation symbol for the fault control action.
324pub fn stream_control_fault_symbol() -> Symbol {
325    Symbol::qualified("stream/fabric", "fault")
326}
327
328fn stream_control_tag_symbol() -> Symbol {
329    Symbol::qualified("stream/fabric-control", "v1")
330}
331
332fn key_expr(name: &str, value: Expr) -> (Expr, Expr) {
333    (Expr::Symbol(Symbol::new(name)), value)
334}
335
336fn map_entries(expr: &Expr) -> Result<&[(Expr, Expr)]> {
337    match expr {
338        Expr::Map(entries) => Ok(entries),
339        other => Err(Error::TypeMismatch {
340            expected: "stream fabric control map",
341            found: expr_kind(other),
342        }),
343    }
344}
345
346fn optional_u64(expr: &Expr) -> Result<Option<u64>> {
347    match expr {
348        Expr::Nil => Ok(None),
349        Expr::String(value) => value
350            .parse::<u64>()
351            .map(Some)
352            .map_err(|err| Error::Eval(format!("invalid stream fabric control limit: {err}"))),
353        other => Err(Error::TypeMismatch {
354            expected: "optional u64 string",
355            found: expr_kind(other),
356        }),
357    }
358}
359
360fn parse_u64(expr: &Expr) -> Result<u64> {
361    match expr {
362        Expr::String(value) => value
363            .parse::<u64>()
364            .map_err(|err| Error::Eval(format!("invalid stream fabric control count: {err}"))),
365        other => Err(Error::TypeMismatch {
366            expected: "u64 string",
367            found: expr_kind(other),
368        }),
369    }
370}
371
372fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
373    sim_value::access::entry_required_sym(entries, name, "symbol field")
374}
375
376fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
377    entries
378        .iter()
379        .find_map(|(key, value)| match key {
380            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
381                Some(value)
382            }
383            _ => None,
384        })
385        .ok_or_else(|| Error::Eval(format!("stream fabric control missing {name} field")))
386}
387
388fn ensure_fields(entries: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
389    for (key, _) in entries {
390        let Expr::Symbol(symbol) = key else {
391            return Err(Error::TypeMismatch {
392                expected: "symbol stream fabric control field",
393                found: expr_kind(key),
394            });
395        };
396        if symbol.namespace.is_none() && allowed.contains(&symbol.name.as_ref()) {
397            continue;
398        }
399        return Err(Error::Eval(format!(
400            "unknown stream fabric control field {}",
401            symbol.as_qualified_str()
402        )));
403    }
404    Ok(())
405}