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