Skip to main content

relaygate_protocol/
frame.rs

1use std::fmt;
2
3use bytes::Bytes;
4
5use crate::{BearerToken, BindingId, Destination, PipeId, SessionId};
6
7/// Stable wire error codes whose discriminants are part of protocol version 3.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(u8)]
10pub enum ErrorCode {
11    /// A request field is malformed or violates the contract.
12    InvalidArgument = 1,
13    /// The operation credential is missing, expired, or unverifiable.
14    Unauthenticated = 2,
15    /// The verified credential does not authorize the operation.
16    PermissionDenied = 3,
17    /// No Binding or Pipe matches the requested Destination or identifier.
18    NotFound = 4,
19    /// The current state does not allow the operation.
20    FailedPrecondition = 5,
21    /// The serving side cannot accept the operation at this time.
22    Unavailable = 6,
23    /// The operation did not reach a terminal result within its deadline.
24    DeadlineExceeded = 7,
25    /// Covers admission, capacity, queue, and size limits.
26    ResourceExhausted = 8,
27    /// The initiator abandoned the operation before it completed.
28    Cancelled = 9,
29    /// A peer violated the wire protocol or state-machine contract.
30    ProtocolError = 10,
31    /// A local invariant or task failed without a peer protocol violation.
32    Internal = 11,
33    /// The requested registration already exists.
34    AlreadyExists = 12,
35}
36
37impl ErrorCode {
38    /// Canonical snake_case name for metric labels and structured logs.
39    #[must_use]
40    pub const fn metric_name(self) -> &'static str {
41        match self {
42            Self::InvalidArgument => "invalid_argument",
43            Self::Unauthenticated => "unauthenticated",
44            Self::PermissionDenied => "permission_denied",
45            Self::NotFound => "not_found",
46            Self::FailedPrecondition => "failed_precondition",
47            Self::Unavailable => "unavailable",
48            Self::DeadlineExceeded => "deadline_exceeded",
49            Self::ResourceExhausted => "resource_exhausted",
50            Self::Cancelled => "cancelled",
51            Self::ProtocolError => "protocol_error",
52            Self::Internal => "internal",
53            Self::AlreadyExists => "already_exists",
54        }
55    }
56
57    /// Decodes the wire byte; `None` for values outside the contract.
58    #[must_use]
59    pub fn from_wire(value: u8) -> Option<Self> {
60        match value {
61            1 => Some(Self::InvalidArgument),
62            2 => Some(Self::Unauthenticated),
63            3 => Some(Self::PermissionDenied),
64            4 => Some(Self::NotFound),
65            5 => Some(Self::FailedPrecondition),
66            6 => Some(Self::Unavailable),
67            7 => Some(Self::DeadlineExceeded),
68            8 => Some(Self::ResourceExhausted),
69            9 => Some(Self::Cancelled),
70            10 => Some(Self::ProtocolError),
71            11 => Some(Self::Internal),
72            12 => Some(Self::AlreadyExists),
73            _ => None,
74        }
75    }
76}
77
78/// Whether a peer may have observed a failed control operation.
79///
80/// This is control-operation metadata, not an application payload receipt.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[repr(u8)]
83pub enum PeerObservation {
84    /// The operation did not reach the peer's observable state.
85    NotObserved = 1,
86    /// The sender cannot determine whether the peer observed the operation.
87    MaybeObserved = 2,
88    /// The peer observed or committed the operation before failure.
89    Observed = 3,
90}
91
92impl PeerObservation {
93    /// Decodes the wire byte; `None` for values outside the contract.
94    #[must_use]
95    pub fn from_wire(value: u8) -> Option<Self> {
96        match value {
97            1 => Some(Self::NotObserved),
98            2 => Some(Self::MaybeObserved),
99            3 => Some(Self::Observed),
100            _ => None,
101        }
102    }
103}
104
105/// Wire messages exchanged within one SDK–Gateway session.
106///
107/// Credentials are operation-scoped and appear only in [`Frame::Publish`] and
108/// [`Frame::Dial`]; application data remains opaque in [`Frame::Data`].
109#[derive(Clone, PartialEq, Eq)]
110pub enum Frame {
111    /// SDK → Gateway: starts a session and carries no credential.
112    Hello,
113    /// Gateway → SDK: establishes the session and assigns its identifier.
114    Welcome {
115        /// Identifies the session incarnation the Gateway just established.
116        session_id: SessionId,
117    },
118    /// Gateway → SDK: refuses the session before it is established.
119    SessionRejected {
120        /// Names the reason the session was refused.
121        code: ErrorCode,
122        /// Human-readable detail for the code.
123        message: String,
124    },
125    /// SDK → Gateway: registers a Destination for this session.
126    Publish {
127        /// Correlates the reply with the request that carried the same id.
128        request_id: u64,
129        /// The exact routing address being registered.
130        destination: Destination,
131        /// Operation credential the Gateway verifies for this registration.
132        access_token: BearerToken,
133    },
134    /// Gateway → SDK: reports that the registration succeeded.
135    Published {
136        /// Correlates the reply with the request that carried the same id.
137        request_id: u64,
138        /// Identifies the Binding the registration created.
139        binding_id: BindingId,
140    },
141    /// Gateway → SDK: reports that the registration failed.
142    PublishFailed {
143        /// Correlates the reply with the request that carried the same id.
144        request_id: u64,
145        /// Names the reason the registration failed.
146        code: ErrorCode,
147        /// Human-readable detail for the code.
148        message: String,
149    },
150    /// SDK → Gateway: releases a Binding this session owns.
151    Unpublish {
152        /// Correlates the reply with the request that carried the same id.
153        request_id: u64,
154        /// Identifies the Binding to release.
155        binding_id: BindingId,
156    },
157    /// Gateway → SDK: confirms that the Binding is released.
158    Unpublished {
159        /// Correlates the reply with the request that carried the same id.
160        request_id: u64,
161    },
162    /// SDK → Gateway: requests a Pipe to a Destination.
163    Dial {
164        /// Session-local id that correlates the dial with its terminal result.
165        connection_id: u64,
166        /// The exact routing address the dial targets.
167        destination: Destination,
168        /// Operation credential the Gateway verifies for this dial.
169        access_token: BearerToken,
170    },
171    /// Offers an incoming Pipe to the session that owns a selected binding.
172    Offer {
173        /// Identifies the Pipe being offered.
174        pipe_id: PipeId,
175        /// Identifies the Binding the Gateway selected for the dial.
176        binding_id: BindingId,
177        /// The routing address the dial targeted.
178        destination: Destination,
179    },
180    /// SDK → Gateway: the Listener admitted the offered Pipe to its queue.
181    OfferAccepted {
182        /// Identifies the offered Pipe.
183        pipe_id: PipeId,
184    },
185    /// SDK → Gateway: the Listener did not admit the offered Pipe.
186    OfferRejected {
187        /// Identifies the offered Pipe.
188        pipe_id: PipeId,
189        /// Names the reason the offer was rejected.
190        code: ErrorCode,
191        /// Human-readable detail for the code.
192        message: String,
193    },
194    /// Confirms that a Pipe is established for the dialing session.
195    Opened {
196        /// Identifies the established Pipe, carrying the origin session id and
197        /// the connection id of the dial that created it.
198        pipe_id: PipeId,
199    },
200    /// Gateway → SDK: ends a dial attempt without a Pipe.
201    DialFailed {
202        /// Correlates the failure with the dial that carried the same id.
203        connection_id: u64,
204        /// Names the reason the dial failed.
205        code: ErrorCode,
206        /// States whether the selected Listener may have observed the dial.
207        observation: PeerObservation,
208        /// Human-readable detail for the code.
209        message: String,
210    },
211    /// Carries opaque application bytes over an established Pipe.
212    Data {
213        /// Identifies the Pipe the bytes belong to.
214        pipe_id: PipeId,
215        /// Application bytes RelayGate relays without interpreting them.
216        payload: Bytes,
217    },
218    /// Half-closes the sender's write direction of a Pipe.
219    Fin {
220        /// Identifies the Pipe being half-closed.
221        pipe_id: PipeId,
222    },
223    /// Closes a Pipe normally in both directions.
224    Close {
225        /// Identifies the Pipe being closed.
226        pipe_id: PipeId,
227    },
228    /// Terminates a Pipe with an error.
229    Reset {
230        /// Identifies the Pipe being terminated.
231        pipe_id: PipeId,
232        /// Names the reason the Pipe was terminated.
233        code: ErrorCode,
234        /// Human-readable detail for the code.
235        message: String,
236    },
237    /// Probes session liveness and expects a matching [`Frame::Pong`].
238    Ping {
239        /// Value the peer echoes in its reply.
240        nonce: u64,
241    },
242    /// Answers a liveness probe.
243    Pong {
244        /// Echoes the nonce of the probe being answered.
245        nonce: u64,
246    },
247    /// Cancels a pending Pipe establishment attempt.
248    Cancel {
249        /// Identifies the pending Pipe to release.
250        pipe_id: PipeId,
251    },
252}
253
254impl fmt::Debug for Frame {
255    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256        match self {
257            Self::Hello => formatter.write_str("Hello"),
258            Self::Welcome { session_id } => formatter
259                .debug_struct("Welcome")
260                .field("session_id", session_id)
261                .finish(),
262            Self::SessionRejected { code, message } => formatter
263                .debug_struct("SessionRejected")
264                .field("code", code)
265                .field("message", message)
266                .finish(),
267            Self::Publish {
268                request_id,
269                destination,
270                access_token,
271            } => formatter
272                .debug_struct("Publish")
273                .field("request_id", request_id)
274                .field("destination", destination)
275                .field("access_token", access_token)
276                .finish(),
277            Self::Published {
278                request_id,
279                binding_id,
280            } => formatter
281                .debug_struct("Published")
282                .field("request_id", request_id)
283                .field("binding_id", binding_id)
284                .finish(),
285            Self::PublishFailed {
286                request_id,
287                code,
288                message,
289            } => formatter
290                .debug_struct("PublishFailed")
291                .field("request_id", request_id)
292                .field("code", code)
293                .field("message", message)
294                .finish(),
295            Self::Unpublish {
296                request_id,
297                binding_id,
298            } => formatter
299                .debug_struct("Unpublish")
300                .field("request_id", request_id)
301                .field("binding_id", binding_id)
302                .finish(),
303            Self::Unpublished { request_id } => formatter
304                .debug_struct("Unpublished")
305                .field("request_id", request_id)
306                .finish(),
307            Self::Dial {
308                connection_id,
309                destination,
310                access_token,
311            } => formatter
312                .debug_struct("Dial")
313                .field("connection_id", connection_id)
314                .field("destination", destination)
315                .field("access_token", access_token)
316                .finish(),
317            Self::Offer {
318                pipe_id,
319                binding_id,
320                destination,
321            } => formatter
322                .debug_struct("Offer")
323                .field("pipe_id", pipe_id)
324                .field("binding_id", binding_id)
325                .field("destination", destination)
326                .finish(),
327            Self::OfferAccepted { pipe_id } => formatter
328                .debug_struct("OfferAccepted")
329                .field("pipe_id", pipe_id)
330                .finish(),
331            Self::OfferRejected {
332                pipe_id,
333                code,
334                message,
335            } => formatter
336                .debug_struct("OfferRejected")
337                .field("pipe_id", pipe_id)
338                .field("code", code)
339                .field("message", message)
340                .finish(),
341            Self::Opened { pipe_id } => formatter
342                .debug_struct("Opened")
343                .field("pipe_id", pipe_id)
344                .finish(),
345            Self::DialFailed {
346                connection_id,
347                code,
348                observation,
349                message,
350            } => formatter
351                .debug_struct("DialFailed")
352                .field("connection_id", connection_id)
353                .field("code", code)
354                .field("observation", observation)
355                .field("message", message)
356                .finish(),
357            Self::Data { pipe_id, payload } => formatter
358                .debug_struct("Data")
359                .field("pipe_id", pipe_id)
360                .field("payload_len", &payload.len())
361                .finish(),
362            Self::Fin { pipe_id } => formatter
363                .debug_struct("Fin")
364                .field("pipe_id", pipe_id)
365                .finish(),
366            Self::Close { pipe_id } => formatter
367                .debug_struct("Close")
368                .field("pipe_id", pipe_id)
369                .finish(),
370            Self::Reset {
371                pipe_id,
372                code,
373                message,
374            } => formatter
375                .debug_struct("Reset")
376                .field("pipe_id", pipe_id)
377                .field("code", code)
378                .field("message", message)
379                .finish(),
380            Self::Ping { nonce } => formatter
381                .debug_struct("Ping")
382                .field("nonce", nonce)
383                .finish(),
384            Self::Pong { nonce } => formatter
385                .debug_struct("Pong")
386                .field("nonce", nonce)
387                .finish(),
388            Self::Cancel { pipe_id } => formatter
389                .debug_struct("Cancel")
390                .field("pipe_id", pipe_id)
391                .finish(),
392        }
393    }
394}