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)]
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}