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 v3 Hello - metadata includes flags.
136 V3 {
137 max_payload_size: u32,
138 initial_channel_credit: u32,
139 } = 0,
140}
141
142/// Metadata value.
143// r[impl call.metadata.type]
144#[repr(u8)]
145#[derive(Debug, Clone, PartialEq, Eq, Facet)]
146pub enum MetadataValue {
147 String(String) = 0,
148 Bytes(Vec<u8>) = 1,
149 U64(u64) = 2,
150}
151
152impl MetadataValue {
153 /// Get the byte length of this value.
154 pub fn byte_len(&self) -> usize {
155 match self {
156 MetadataValue::String(s) => s.len(),
157 MetadataValue::Bytes(b) => b.len(),
158 MetadataValue::U64(_) => 8,
159 }
160 }
161}
162
163/// Metadata entry flags.
164///
165/// r[impl call.metadata.flags] - Flags control metadata handling behavior.
166pub mod metadata_flags {
167 /// No special handling.
168 pub const NONE: u64 = 0;
169
170 /// Value MUST NOT be logged, traced, or included in error messages.
171 pub const SENSITIVE: u64 = 1 << 0;
172
173 /// Value MUST NOT be forwarded to downstream calls.
174 pub const NO_PROPAGATE: u64 = 1 << 1;
175}
176
177/// Metadata validation limits.
178///
179/// r[impl call.metadata.limits] - Metadata has size limits.
180pub mod metadata_limits {
181 /// Maximum number of metadata entries.
182 pub const MAX_ENTRIES: usize = 128;
183 /// Maximum key size in bytes.
184 pub const MAX_KEY_SIZE: usize = 256;
185 /// Maximum value size in bytes (16 KB).
186 pub const MAX_VALUE_SIZE: usize = 16 * 1024;
187 /// Maximum total metadata size in bytes (64 KB).
188 pub const MAX_TOTAL_SIZE: usize = 64 * 1024;
189}
190
191/// Validate metadata against protocol limits.
192///
193/// r[impl call.metadata.limits] - Validate all metadata constraints.
194/// r[impl call.metadata.keys] - Keys at most 256 bytes.
195/// r[impl call.metadata.order] - Order is preserved (Vec maintains order).
196/// r[impl call.metadata.duplicates] - Duplicate keys are allowed.
197pub fn validate_metadata(metadata: &[(String, MetadataValue, u64)]) -> Result<(), &'static str> {
198 use metadata_limits::*;
199
200 // Check entry count
201 if metadata.len() > MAX_ENTRIES {
202 return Err("call.metadata.limits");
203 }
204
205 let mut total_size = 0usize;
206
207 for (key, value, _flags) in metadata {
208 // Check key size
209 if key.len() > MAX_KEY_SIZE {
210 return Err("call.metadata.limits");
211 }
212
213 // Check value size
214 let value_len = value.byte_len();
215 if value_len > MAX_VALUE_SIZE {
216 return Err("call.metadata.limits");
217 }
218
219 // Accumulate total size (flags are varint-encoded, typically 1 byte)
220 total_size += key.len() + value_len;
221 }
222
223 // Check total size
224 if total_size > MAX_TOTAL_SIZE {
225 return Err("call.metadata.limits");
226 }
227
228 Ok(())
229}
230
231/// Metadata entry: (key, value, flags).
232///
233/// r[impl call.metadata.type] - Metadata is a list of entries.
234/// r[impl call.metadata.flags] - Each entry includes flags for handling behavior.
235pub type Metadata = Vec<(String, MetadataValue, u64)>;
236
237/// Protocol message.
238///
239/// Variant order is wire-significant (postcard enum discriminants).
240///
241/// # Virtual Connections (v2.0.0)
242///
243/// A link carries multiple virtual connections, each with its own request ID
244/// space, channel ID space, and dispatcher. Connection 0 is implicit on link
245/// establishment. Additional connections are opened via Connect/Accept/Reject.
246///
247/// All messages except Hello, Connect, Accept, and Reject include a `conn_id`
248/// field identifying which virtual connection they belong to.
249#[repr(u8)]
250#[derive(Debug, Clone, PartialEq, Eq, Facet)]
251pub enum Message {
252 // ========================================================================
253 // Link control (no conn_id - applies to entire link)
254 // ========================================================================
255 /// r[impl message.hello.timing] - Sent immediately after link establishment.
256 Hello(Hello) = 0,
257
258 // ========================================================================
259 // Virtual connection control
260 // ========================================================================
261 /// r[impl message.connect.initiate] - Request a new virtual connection.
262 Connect { request_id: u64, metadata: Metadata } = 1,
263
264 /// r[impl message.accept.response] - Accept a virtual connection request.
265 Accept {
266 request_id: u64,
267 conn_id: ConnectionId,
268 metadata: Metadata,
269 } = 2,
270
271 /// r[impl message.reject.response] - Reject a virtual connection request.
272 Reject {
273 request_id: u64,
274 reason: String,
275 metadata: Metadata,
276 } = 3,
277
278 // ========================================================================
279 // Connection control (conn_id scoped)
280 // ========================================================================
281 /// r[impl message.goodbye.send] - Close a virtual connection.
282 /// r[impl message.goodbye.connection-zero] - Goodbye on conn 0 closes entire link.
283 Goodbye {
284 conn_id: ConnectionId,
285 reason: String,
286 } = 4,
287
288 // ========================================================================
289 // RPC (conn_id scoped)
290 // ========================================================================
291 /// r[impl core.metadata] - Request carries metadata key-value pairs.
292 /// r[impl call.metadata.unknown] - Unknown keys are ignored.
293 /// r[impl channeling.request.channels] - Channel IDs listed explicitly for proxy support.
294 Request {
295 conn_id: ConnectionId,
296 request_id: u64,
297 method_id: u64,
298 metadata: Metadata,
299 /// Channel IDs used by this call, in argument declaration order.
300 /// This is the authoritative source - servers MUST use these IDs,
301 /// not any IDs that may be embedded in the payload.
302 channels: Vec<u64>,
303 payload: Vec<u8>,
304 } = 5,
305
306 /// r[impl core.metadata] - Response carries metadata key-value pairs.
307 /// r[impl call.metadata.unknown] - Unknown keys are ignored.
308 Response {
309 conn_id: ConnectionId,
310 request_id: u64,
311 metadata: Metadata,
312 /// Channel IDs for streams in the response, in return type declaration order.
313 /// Client uses these to bind receivers for incoming Data messages.
314 channels: Vec<u64>,
315 payload: Vec<u8>,
316 } = 6,
317
318 /// r[impl call.cancel.message] - Cancel message requests callee stop processing.
319 /// r[impl call.cancel.no-response-required] - Caller should timeout, not wait indefinitely.
320 Cancel {
321 conn_id: ConnectionId,
322 request_id: u64,
323 } = 7,
324
325 // ========================================================================
326 // Channels (conn_id scoped)
327 // ========================================================================
328 // r[impl wire.stream] - Tx<T>/Rx<T> encoded as u64 channel ID on wire
329 Data {
330 conn_id: ConnectionId,
331 channel_id: u64,
332 payload: Vec<u8>,
333 } = 8,
334
335 Close {
336 conn_id: ConnectionId,
337 channel_id: u64,
338 } = 9,
339
340 Reset {
341 conn_id: ConnectionId,
342 channel_id: u64,
343 } = 10,
344
345 Credit {
346 conn_id: ConnectionId,
347 channel_id: u64,
348 bytes: u32,
349 } = 11,
350}