Skip to main content

roam_types/
rpc_plan.rs

1use facet::{Facet, Shape};
2use facet_path::{Path, walk_shape};
3use std::collections::HashMap;
4use std::sync::{Mutex, OnceLock};
5
6use crate::channel;
7
8/// Precomputed plan for an RPC type (args, response, or error).
9///
10/// Contains the shape and locations of all channels within the type structure.
11/// Deserialization plans are cached transparently by facet via `TypePlanCore::from_shape`.
12pub struct RpcPlan {
13    /// The shape this plan was built for. Used for type-safe construction.
14    pub shape: &'static Shape,
15
16    /// Locations of all Rx/Tx channels in this type, in declaration order.
17    pub channel_locations: &'static [ChannelLocation],
18}
19
20/// A precomputed location of a channel within a type structure.
21pub struct ChannelLocation {
22    /// Path from the root to this channel.
23    pub path: Path,
24
25    /// Whether this is an Rx or Tx channel.
26    pub kind: ChannelKind,
27
28    /// Initial credit for this channel, from the const generic `N`.
29    pub initial_credit: u32,
30}
31
32/// The kind of a channel.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ChannelKind {
35    Rx,
36    Tx,
37}
38
39impl RpcPlan {
40    fn from_shape(shape: &'static Shape) -> Self {
41        let mut visitor = ChannelDiscovery {
42            locations: Vec::new(),
43        };
44        walk_shape(shape, &mut visitor);
45
46        RpcPlan {
47            shape,
48            channel_locations: visitor.locations.leak(),
49        }
50    }
51
52    /// Return a process-global cached plan for the given shape.
53    pub fn for_shape(shape: &'static Shape) -> &'static Self {
54        static CACHE: OnceLock<Mutex<HashMap<usize, &'static RpcPlan>>> = OnceLock::new();
55        let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
56
57        let key = shape as *const Shape as usize;
58
59        let mut guard = cache
60            .lock()
61            .expect("rpc plan cache mutex should not be poisoned");
62        if let Some(plan) = guard.get(&key) {
63            return plan;
64        }
65
66        let plan = Box::leak(Box::new(Self::from_shape(shape)));
67        guard.insert(key, plan);
68        plan
69    }
70
71    /// Return a process-global cached plan for a concrete type.
72    pub fn for_type<T: Facet<'static>>() -> &'static Self {
73        Self::for_shape(T::SHAPE)
74    }
75}
76
77/// Extract the initial credit `N` from a Tx/Rx shape's const params.
78fn extract_initial_credit(shape: &'static Shape) -> u32 {
79    shape
80        .const_params
81        .iter()
82        .find(|cp| cp.name == "N")
83        .map(|cp| cp.value as u32)
84        .unwrap_or(16)
85}
86
87/// Visitor that discovers Rx/Tx channel locations in a type structure.
88// r[impl rpc.channel.discovery]
89// r[impl rpc.channel.no-collections]
90struct ChannelDiscovery {
91    locations: Vec<ChannelLocation>,
92}
93
94impl facet_path::ShapeVisitor for ChannelDiscovery {
95    fn enter(&mut self, path: &Path, shape: &'static Shape) -> facet_path::VisitDecision {
96        if channel::is_tx(shape) {
97            self.locations.push(ChannelLocation {
98                path: path.clone(),
99                kind: ChannelKind::Tx,
100                initial_credit: extract_initial_credit(shape),
101            });
102            return facet_path::VisitDecision::SkipChildren;
103        }
104
105        if channel::is_rx(shape) {
106            self.locations.push(ChannelLocation {
107                path: path.clone(),
108                kind: ChannelKind::Rx,
109                initial_credit: extract_initial_credit(shape),
110            });
111            return facet_path::VisitDecision::SkipChildren;
112        }
113
114        // Skip all collection subtrees — schema-driven discovery only
115        if matches!(
116            shape.def,
117            facet::Def::List(_) | facet::Def::Array(_) | facet::Def::Map(_) | facet::Def::Set(_)
118        ) {
119            return facet_path::VisitDecision::SkipChildren;
120        }
121
122        facet_path::VisitDecision::Recurse
123    }
124}