Skip to main content

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}