uptrakit_wire/envelope.rs
1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use super::messages::{ControllerMessage, ServiceMessage};
5use super::trace_context::TraceContext;
6
7/// The current wire protocol version stamped on every envelope.
8///
9/// Increment this constant whenever a breaking change is introduced to the
10/// wire protocol (e.g. a required field is added, a variant renamed, or
11/// capability-negotiation semantics change). Peers that receive a
12/// `protocol_version` value they do not recognise must close the connection
13/// with [`CloseReason::ProtocolError`](super::CloseReason).
14pub const CURRENT_PROTOCOL_VERSION: u32 = 1;
15
16/// Pagination metadata for a paginated report.
17///
18/// When a service needs to send a report that exceeds the WebSocket frame
19/// limit, it splits the payload into pages. Each page carries the same
20/// `report_id` and a 1-based `page` number out of `total_pages`.
21///
22/// The controller processes each page immediately (no payload buffering) and
23/// defers only lightweight finalization (e.g. notification emission) until the
24/// final page arrives.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub struct ReportPagination {
27 /// Unique identifier grouping all pages of the same logical report.
28 pub report_id: Uuid,
29 /// 1-based page number within the report.
30 pub page: u32,
31 /// Total number of pages in the report (known upfront by the sender).
32 pub total_pages: u32,
33}
34
35/// Envelope wrapping a [`ServiceMessage`] with a monotonically increasing
36/// sequence number for replay protection and the current protocol version.
37///
38/// JSON on the wire includes an optional `trace_context` object for distributed
39/// tracing correlation, and optional pagination metadata for paginated reports.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ServiceEnvelope {
42 pub protocol_version: u32,
43 pub seq: u64,
44 /// Distributed tracing context for correlating this message across services.
45 /// Always populated when sending; tolerates absence when receiving from older peers.
46 #[serde(default)]
47 pub trace_context: TraceContext,
48 /// Pagination metadata for paginated reports.
49 ///
50 /// `None` for single-message reports (the common case). When present, the
51 /// controller tracks page arrival and defers finalization until all pages
52 /// have been received.
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub pagination: Option<ReportPagination>,
55 #[serde(flatten)]
56 pub message: ServiceMessage,
57}
58
59/// Envelope wrapping a [`ControllerMessage`] with a monotonically increasing
60/// sequence number for replay protection and the current protocol version.
61///
62/// JSON on the wire includes an optional `trace_context` object for distributed
63/// tracing correlation.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ControllerEnvelope {
66 pub protocol_version: u32,
67 pub seq: u64,
68 /// Distributed tracing context for correlating this message across services.
69 /// Always populated when sending; tolerates absence when receiving from older peers.
70 #[serde(default)]
71 pub trace_context: TraceContext,
72 #[serde(flatten)]
73 pub message: ControllerMessage,
74}
75
76/// Tracks outgoing sequence numbers for a single direction of a WebSocket
77/// connection. Assigns monotonically increasing numbers starting at 1.
78#[derive(Debug)]
79pub struct OutgoingSeq {
80 next: u64,
81}
82
83impl OutgoingSeq {
84 /// Create a new outgoing sequence counter (first message gets seq 1).
85 pub fn new() -> Self {
86 Self { next: 1 }
87 }
88
89 /// Wrap a [`ServiceMessage`] in a [`ServiceEnvelope`], assigning the next
90 /// sequence number, stamping [`CURRENT_PROTOCOL_VERSION`], and attaching
91 /// the given [`TraceContext`] for distributed tracing.
92 pub fn wrap_service(
93 &mut self,
94 message: ServiceMessage,
95 trace_context: TraceContext,
96 ) -> ServiceEnvelope {
97 self.wrap_service_paginated(message, trace_context, None)
98 }
99
100 /// Wrap a [`ServiceMessage`] in a [`ServiceEnvelope`] with optional
101 /// pagination metadata.
102 pub fn wrap_service_paginated(
103 &mut self,
104 message: ServiceMessage,
105 trace_context: TraceContext,
106 pagination: Option<ReportPagination>,
107 ) -> ServiceEnvelope {
108 let seq = self.next;
109 self.next += 1;
110 ServiceEnvelope {
111 protocol_version: CURRENT_PROTOCOL_VERSION,
112 seq,
113 trace_context,
114 pagination,
115 message,
116 }
117 }
118
119 /// Wrap a [`ControllerMessage`] in a [`ControllerEnvelope`], assigning the
120 /// next sequence number, stamping [`CURRENT_PROTOCOL_VERSION`], and attaching
121 /// the given [`TraceContext`] for distributed tracing.
122 pub fn wrap_controller(
123 &mut self,
124 message: ControllerMessage,
125 trace_context: TraceContext,
126 ) -> ControllerEnvelope {
127 let seq = self.next;
128 self.next += 1;
129 ControllerEnvelope {
130 protocol_version: CURRENT_PROTOCOL_VERSION,
131 seq,
132 trace_context,
133 message,
134 }
135 }
136}
137
138impl Default for OutgoingSeq {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144/// Validates incoming sequence numbers for a single direction of a WebSocket
145/// connection. Expects messages to arrive as 1, 2, 3, ...
146#[derive(Debug)]
147pub struct IncomingSeq {
148 expected: u64,
149}
150
151impl IncomingSeq {
152 /// Create a new incoming sequence validator (first expected seq is 1).
153 pub fn new() -> Self {
154 Self { expected: 1 }
155 }
156
157 /// Validate that the received sequence number matches the expected value.
158 ///
159 /// On success, advances the expected counter. On failure, returns a
160 /// [`SeqError`] describing the mismatch.
161 pub fn validate(&mut self, received: u64) -> Result<(), SeqError> {
162 if received != self.expected {
163 return Err(SeqError {
164 expected: self.expected,
165 received,
166 });
167 }
168 self.expected += 1;
169 Ok(())
170 }
171}
172
173impl Default for IncomingSeq {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179/// Error returned when a received sequence number does not match the expected
180/// value.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
182#[error("sequence error: expected {expected}, received {received}")]
183pub struct SeqError {
184 pub expected: u64,
185 pub received: u64,
186}