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}