roam_wire/
lib.rs

1#![deny(unsafe_code)]
2
3//! Spec-level wire types.
4//!
5//! Canonical definitions live in `docs/content/spec/_index.md` and `docs/content/shm-spec/_index.md`.
6
7use facet::Facet;
8
9/// Connection ID identifying a virtual connection on a link.
10///
11/// Connection 0 is the root connection, established implicitly when the link is created.
12/// Additional connections are opened via Connect/Accept messages.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Facet)]
14#[repr(transparent)]
15pub struct ConnectionId(pub u64);
16
17impl ConnectionId {
18    /// The root connection (always exists on a link).
19    pub const ROOT: Self = Self(0);
20
21    /// Create a new connection ID.
22    pub const fn new(id: u64) -> Self {
23        Self(id)
24    }
25
26    /// Get the raw u64 value.
27    pub const fn raw(self) -> u64 {
28        self.0
29    }
30
31    /// Check if this is the root connection.
32    pub const fn is_root(self) -> bool {
33        self.0 == 0
34    }
35}
36
37impl From<u64> for ConnectionId {
38    fn from(id: u64) -> Self {
39        Self(id)
40    }
41}
42
43impl From<ConnectionId> for u64 {
44    fn from(id: ConnectionId) -> Self {
45        id.0
46    }
47}
48
49impl std::fmt::Display for ConnectionId {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "conn:{}", self.0)
52    }
53}
54
55/// Request ID identifying an in-flight RPC request.
56///
57/// Request IDs are unique within a connection and monotonically increasing.
58/// r[impl call.request-id.uniqueness]
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Facet)]
60#[repr(transparent)]
61pub struct RequestId(pub u64);
62
63impl RequestId {
64    /// Create a new request ID.
65    pub const fn new(id: u64) -> Self {
66        Self(id)
67    }
68
69    /// Get the raw u64 value.
70    pub const fn raw(self) -> u64 {
71        self.0
72    }
73}
74
75impl From<u64> for RequestId {
76    fn from(id: u64) -> Self {
77        Self(id)
78    }
79}
80
81impl From<RequestId> for u64 {
82    fn from(id: RequestId) -> Self {
83        id.0
84    }
85}
86
87impl std::fmt::Display for RequestId {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "req:{}", self.0)
90    }
91}
92
93/// Method ID identifying an RPC method.
94///
95/// Method IDs are computed as a hash of the service and method names.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Facet)]
97#[repr(transparent)]
98pub struct MethodId(pub u64);
99
100impl MethodId {
101    /// Create a new method ID.
102    pub const fn new(id: u64) -> Self {
103        Self(id)
104    }
105
106    /// Get the raw u64 value.
107    pub const fn raw(self) -> u64 {
108        self.0
109    }
110}
111
112impl From<u64> for MethodId {
113    fn from(id: u64) -> Self {
114        Self(id)
115    }
116}
117
118impl From<MethodId> for u64 {
119    fn from(id: MethodId) -> Self {
120        id.0
121    }
122}
123
124impl std::fmt::Display for MethodId {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "method:{}", self.0)
127    }
128}
129
130/// Hello message for handshake.
131// r[impl message.hello.structure]
132#[repr(u8)]
133#[derive(Debug, Clone, PartialEq, Eq, Facet)]
134pub enum Hello {
135    /// Spec v1 Hello - deprecated, will be rejected.
136    V1 {
137        max_payload_size: u32,
138        initial_channel_credit: u32,
139    } = 0,
140
141    /// Spec v2 Hello - supports virtual connections.
142    V2 {
143        max_payload_size: u32,
144        initial_channel_credit: u32,
145    } = 1,
146}
147
148/// Metadata value.
149// r[impl call.metadata.type]
150#[repr(u8)]
151#[derive(Debug, Clone, PartialEq, Eq, Facet)]
152pub enum MetadataValue {
153    String(String) = 0,
154    Bytes(Vec<u8>) = 1,
155    U64(u64) = 2,
156}
157
158impl MetadataValue {
159    /// Get the byte length of this value.
160    pub fn byte_len(&self) -> usize {
161        match self {
162            MetadataValue::String(s) => s.len(),
163            MetadataValue::Bytes(b) => b.len(),
164            MetadataValue::U64(_) => 8,
165        }
166    }
167}
168
169/// Metadata validation limits.
170///
171/// r[impl call.metadata.limits] - Metadata has size limits.
172pub mod metadata_limits {
173    /// Maximum number of metadata entries.
174    pub const MAX_ENTRIES: usize = 128;
175    /// Maximum key size in bytes.
176    pub const MAX_KEY_SIZE: usize = 256;
177    /// Maximum value size in bytes (16 KB).
178    pub const MAX_VALUE_SIZE: usize = 16 * 1024;
179    /// Maximum total metadata size in bytes (64 KB).
180    pub const MAX_TOTAL_SIZE: usize = 64 * 1024;
181}
182
183/// Validate metadata against protocol limits.
184///
185/// r[impl call.metadata.limits] - Validate all metadata constraints.
186/// r[impl call.metadata.keys] - Keys at most 256 bytes.
187/// r[impl call.metadata.order] - Order is preserved (Vec maintains order).
188/// r[impl call.metadata.duplicates] - Duplicate keys are allowed.
189pub fn validate_metadata(metadata: &[(String, MetadataValue)]) -> Result<(), &'static str> {
190    use metadata_limits::*;
191
192    // Check entry count
193    if metadata.len() > MAX_ENTRIES {
194        return Err("call.metadata.limits");
195    }
196
197    let mut total_size = 0usize;
198
199    for (key, value) in metadata {
200        // Check key size
201        if key.len() > MAX_KEY_SIZE {
202            return Err("call.metadata.limits");
203        }
204
205        // Check value size
206        let value_len = value.byte_len();
207        if value_len > MAX_VALUE_SIZE {
208            return Err("call.metadata.limits");
209        }
210
211        // Accumulate total size
212        total_size += key.len() + value_len;
213    }
214
215    // Check total size
216    if total_size > MAX_TOTAL_SIZE {
217        return Err("call.metadata.limits");
218    }
219
220    Ok(())
221}
222
223/// Metadata type alias for convenience.
224pub type Metadata = Vec<(String, MetadataValue)>;
225
226/// Protocol message.
227///
228/// Variant order is wire-significant (postcard enum discriminants).
229///
230/// # Virtual Connections (v2.0.0)
231///
232/// A link carries multiple virtual connections, each with its own request ID
233/// space, channel ID space, and dispatcher. Connection 0 is implicit on link
234/// establishment. Additional connections are opened via Connect/Accept/Reject.
235///
236/// All messages except Hello, Connect, Accept, and Reject include a `conn_id`
237/// field identifying which virtual connection they belong to.
238#[repr(u8)]
239#[derive(Debug, Clone, PartialEq, Eq, Facet)]
240pub enum Message {
241    // ========================================================================
242    // Link control (no conn_id - applies to entire link)
243    // ========================================================================
244    /// r[impl message.hello.timing] - Sent immediately after link establishment.
245    Hello(Hello) = 0,
246
247    // ========================================================================
248    // Virtual connection control
249    // ========================================================================
250    /// r[impl message.connect.initiate] - Request a new virtual connection.
251    Connect { request_id: u64, metadata: Metadata } = 1,
252
253    /// r[impl message.accept.response] - Accept a virtual connection request.
254    Accept {
255        request_id: u64,
256        conn_id: ConnectionId,
257        metadata: Metadata,
258    } = 2,
259
260    /// r[impl message.reject.response] - Reject a virtual connection request.
261    Reject {
262        request_id: u64,
263        reason: String,
264        metadata: Metadata,
265    } = 3,
266
267    // ========================================================================
268    // Connection control (conn_id scoped)
269    // ========================================================================
270    /// r[impl message.goodbye.send] - Close a virtual connection.
271    /// r[impl message.goodbye.connection-zero] - Goodbye on conn 0 closes entire link.
272    Goodbye {
273        conn_id: ConnectionId,
274        reason: String,
275    } = 4,
276
277    // ========================================================================
278    // RPC (conn_id scoped)
279    // ========================================================================
280    /// r[impl core.metadata] - Request carries metadata key-value pairs.
281    /// r[impl call.metadata.unknown] - Unknown keys are ignored.
282    /// r[impl channeling.request.channels] - Channel IDs listed explicitly for proxy support.
283    Request {
284        conn_id: ConnectionId,
285        request_id: u64,
286        method_id: u64,
287        metadata: Metadata,
288        /// Channel IDs used by this call, in argument declaration order.
289        /// This is the authoritative source - servers MUST use these IDs,
290        /// not any IDs that may be embedded in the payload.
291        channels: Vec<u64>,
292        payload: Vec<u8>,
293    } = 5,
294
295    /// r[impl core.metadata] - Response carries metadata key-value pairs.
296    /// r[impl call.metadata.unknown] - Unknown keys are ignored.
297    Response {
298        conn_id: ConnectionId,
299        request_id: u64,
300        metadata: Metadata,
301        /// Channel IDs for streams in the response, in return type declaration order.
302        /// Client uses these to bind receivers for incoming Data messages.
303        channels: Vec<u64>,
304        payload: Vec<u8>,
305    } = 6,
306
307    /// r[impl call.cancel.message] - Cancel message requests callee stop processing.
308    /// r[impl call.cancel.no-response-required] - Caller should timeout, not wait indefinitely.
309    Cancel {
310        conn_id: ConnectionId,
311        request_id: u64,
312    } = 7,
313
314    // ========================================================================
315    // Channels (conn_id scoped)
316    // ========================================================================
317    // r[impl wire.stream] - Tx<T>/Rx<T> encoded as u64 channel ID on wire
318    Data {
319        conn_id: ConnectionId,
320        channel_id: u64,
321        payload: Vec<u8>,
322    } = 8,
323
324    Close {
325        conn_id: ConnectionId,
326        channel_id: u64,
327    } = 9,
328
329    Reset {
330        conn_id: ConnectionId,
331        channel_id: u64,
332    } = 10,
333
334    Credit {
335        conn_id: ConnectionId,
336        channel_id: u64,
337        bytes: u32,
338    } = 11,
339}