1use serde::{Deserialize, Serialize};
2
3pub const MANAGED_REQUEST_VERSION: u32 = 1;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct MutationPosition {
14 pub topic_generation: u64,
15 pub partition: u32,
16 pub offset: u64,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ManagedRequestEnvelope {
21 pub v: u32,
22 pub operation_id: u128,
23 #[serde(with = "crate::encoding::bin_bytes")]
24 pub payload: Vec<u8>,
25}
26
27impl ManagedRequestEnvelope {
28 pub fn validate(&self) -> Result<(), &'static str> {
29 if self.v != MANAGED_REQUEST_VERSION {
30 return Err("unsupported managed request version");
31 }
32 if self.operation_id == 0 {
33 return Err("managed request operation id must not be zero");
34 }
35 Ok(())
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum PositionSurface {
43 Kv,
44 Fork,
45 Graph,
46}
47
48impl PositionSurface {
49 pub const fn of_code(code: u32) -> Option<Self> {
52 use crate::codes::{
53 AGDX_FORK_CREATE_CODE, AGDX_FORK_DELETE_CODE, AGDX_FORK_LIST_CODE,
54 AGDX_FORK_PROMOTE_CODE, AGDX_FORK_PUT_CODE, AGDX_GRAPH_NEIGHBORS_CODE,
55 AGDX_GRAPH_QUERY_CODE, AGDX_GRAPH_UPSERT_CODE, AGDX_KV_CAS_CODE,
56 AGDX_KV_CAS_FENCED_CODE, AGDX_KV_COPY_CODE, AGDX_KV_DELETE_CODE,
57 AGDX_KV_DELETE_MANY_CODE, AGDX_KV_EXISTS_CODE, AGDX_KV_EXPIRE_CODE, AGDX_KV_GET_CODE,
58 AGDX_KV_LEASE_CODE, AGDX_KV_LEASE_RENEW_CODE, AGDX_KV_MOVE_CODE,
59 AGDX_KV_NAMESPACES_CODE, AGDX_KV_PATCH_CODE, AGDX_KV_RELEASE_CODE, AGDX_KV_SCAN_CODE,
60 AGDX_KV_SET_CODE,
61 };
62 match code {
63 AGDX_KV_GET_CODE
64 | AGDX_KV_SCAN_CODE
65 | AGDX_KV_NAMESPACES_CODE
66 | AGDX_KV_EXISTS_CODE
67 | AGDX_KV_SET_CODE
68 | AGDX_KV_CAS_CODE
69 | AGDX_KV_DELETE_CODE
70 | AGDX_KV_DELETE_MANY_CODE
71 | AGDX_KV_EXPIRE_CODE
72 | AGDX_KV_PATCH_CODE
73 | AGDX_KV_COPY_CODE
74 | AGDX_KV_MOVE_CODE
75 | AGDX_KV_CAS_FENCED_CODE
76 | AGDX_KV_LEASE_CODE
77 | AGDX_KV_LEASE_RENEW_CODE
78 | AGDX_KV_RELEASE_CODE => Some(Self::Kv),
79 AGDX_FORK_LIST_CODE
80 | AGDX_FORK_CREATE_CODE
81 | AGDX_FORK_DELETE_CODE
82 | AGDX_FORK_PROMOTE_CODE
83 | AGDX_FORK_PUT_CODE => Some(Self::Fork),
84 AGDX_GRAPH_QUERY_CODE | AGDX_GRAPH_NEIGHBORS_CODE | AGDX_GRAPH_UPSERT_CODE => {
85 Some(Self::Graph)
86 }
87 _ => None,
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct ManagedReadEnvelope {
107 pub v: u32,
108 #[serde(with = "crate::encoding::bin_bytes")]
110 pub read: Vec<u8>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub min_position: Option<MutationPosition>,
113 #[serde(default, skip_serializing_if = "is_false")]
114 pub log_head: bool,
115 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub min_sessions: Vec<SessionPosition>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct SessionPosition {
127 pub stream_id: u32,
128 pub stream_generation: u64,
130 pub topic: String,
131 pub topic_generation: u64,
133 pub partition_id: u32,
134 pub offset: u64,
136}
137
138#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct AppliedFrontier {
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub kv: Option<MutationPosition>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub fork: Option<MutationPosition>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub graph: Option<MutationPosition>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub sessions: Vec<SessionPosition>,
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ManagedReplyEnvelope {
164 pub v: u32,
165 #[serde(default)]
166 pub frontier: AppliedFrontier,
167 #[serde(with = "crate::encoding::bin_bytes")]
168 pub reply: Vec<u8>,
169}
170
171#[expect(clippy::trivially_copy_pass_by_ref, reason = "serde skip predicate")]
172fn is_false(value: &bool) -> bool {
173 !*value
174}
175
176#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct MutationCommandEnvelope {
182 pub v: u32,
183 pub operation_id: u128,
184 pub actor_user_id: u32,
186 pub deployment_id: u64,
187 pub timestamp_micros: u64,
188 pub command_code: u32,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub scope: Option<crate::forward::ForwardedScope>,
192 #[serde(with = "crate::encoding::bin_bytes")]
193 pub payload: Vec<u8>,
194}
195
196impl MutationCommandEnvelope {
197 pub fn validate(&self) -> Result<(), &'static str> {
198 if self.v == 0 {
199 return Err("mutation command version must not be zero");
200 }
201 if self.operation_id == 0 {
202 return Err("mutation command operation id must not be zero");
203 }
204 Ok(())
205 }
206}
207
208#[cfg(all(test, feature = "cbor"))]
209mod tests {
210 use super::*;
211 use crate::codes::{AGDX_KV_SET_CODE, KV_OP_VERSION};
212 use crate::framing::{decode_named, encode_named};
213
214 #[test]
215 fn given_a_read_envelope_when_round_tripped_then_should_keep_the_inner_read() {
216 let get = crate::kv::KvGet {
217 v: KV_OP_VERSION,
218 namespace: "orders".to_owned(),
219 key: b"k".to_vec(),
220 if_none_match: None,
221 min_position: None,
222 };
223 let read = encode_named(&get).expect("encodes");
224 let envelope = ManagedReadEnvelope {
225 v: MANAGED_REQUEST_VERSION,
226 read: read.clone(),
227 min_position: Some(MutationPosition {
228 topic_generation: 3,
229 partition: 0,
230 offset: 41,
231 }),
232 log_head: true,
233 min_sessions: vec![SessionPosition {
234 stream_id: 2,
235 stream_generation: 10,
236 topic: "agent.sessions".to_owned(),
237 topic_generation: 11,
238 partition_id: 1,
239 offset: 7,
240 }],
241 };
242 let decoded: ManagedReadEnvelope =
243 decode_named(&encode_named(&envelope).expect("encodes")).expect("decodes");
244 assert_eq!(decoded, envelope);
245 assert!(decode_named::<ManagedReadEnvelope>(&read).is_err());
248 let managed = encode_named(&ManagedRequestEnvelope {
249 v: MANAGED_REQUEST_VERSION,
250 operation_id: 9,
251 payload: read,
252 })
253 .expect("encodes");
254 assert!(decode_named::<ManagedReadEnvelope>(&managed).is_err());
255 }
256
257 #[test]
258 fn given_a_reply_envelope_when_round_tripped_then_should_keep_the_reply_and_refuse_plain_replies()
259 {
260 let reply =
261 encode_named(&crate::kv::KvReply::Ok(crate::kv::KvOutcome::Written)).expect("encodes");
262 let envelope = ManagedReplyEnvelope {
263 v: MANAGED_REQUEST_VERSION,
264 frontier: AppliedFrontier {
265 kv: Some(MutationPosition {
266 topic_generation: 1,
267 partition: 0,
268 offset: 7,
269 }),
270 fork: None,
271 graph: None,
272 sessions: Vec::new(),
273 },
274 reply: reply.clone(),
275 };
276 let decoded: ManagedReplyEnvelope =
277 decode_named(&encode_named(&envelope).expect("encodes")).expect("decodes");
278 assert_eq!(decoded, envelope);
279 assert!(decode_named::<ManagedReplyEnvelope>(&reply).is_err());
280 let refusal = encode_named(&crate::result::CommandError::new(
281 crate::result::ResultCode::Stale,
282 "behind",
283 ))
284 .expect("encodes");
285 assert!(decode_named::<ManagedReplyEnvelope>(&refusal).is_err());
286 }
287
288 #[test]
289 fn given_command_codes_when_classified_then_should_name_their_surface() {
290 use crate::codes::{
291 AGDX_FORK_LIST_CODE, AGDX_GRAPH_UPSERT_CODE, AGDX_KV_LEASE_CODE, AGDX_QUERY_CODE,
292 AGDX_SESSION_GET_CODE,
293 };
294 assert_eq!(
295 PositionSurface::of_code(AGDX_KV_LEASE_CODE),
296 Some(PositionSurface::Kv)
297 );
298 assert_eq!(
299 PositionSurface::of_code(AGDX_FORK_LIST_CODE),
300 Some(PositionSurface::Fork)
301 );
302 assert_eq!(
303 PositionSurface::of_code(AGDX_GRAPH_UPSERT_CODE),
304 Some(PositionSurface::Graph)
305 );
306 assert_eq!(PositionSurface::of_code(AGDX_SESSION_GET_CODE), None);
307 assert_eq!(PositionSurface::of_code(AGDX_QUERY_CODE), None);
308 }
309
310 #[test]
311 fn given_a_command_envelope_when_round_tripped_then_should_preserve_request_bytes() {
312 let envelope = MutationCommandEnvelope {
313 v: KV_OP_VERSION,
314 operation_id: 42,
315 actor_user_id: 7,
316 deployment_id: 3,
317 timestamp_micros: 1_700_000_000_000_000,
318 command_code: AGDX_KV_SET_CODE,
319 scope: None,
320 payload: vec![0, 1, 2, 255],
321 };
322 let bytes = encode_named(&envelope).expect("encodes");
323 let back: MutationCommandEnvelope = decode_named(&bytes).expect("decodes");
324 back.validate().expect("the envelope is valid");
325 assert_eq!(back.v, KV_OP_VERSION);
326 assert_eq!(back.operation_id, 42);
327 assert_eq!(back.actor_user_id, 7);
328 assert_eq!(back.deployment_id, 3);
329 assert_eq!(back.timestamp_micros, 1_700_000_000_000_000);
330 assert_eq!(back.command_code, AGDX_KV_SET_CODE);
331 assert_eq!(back.payload, vec![0, 1, 2, 255]);
332 }
333
334 #[test]
335 fn given_a_managed_request_when_round_tripped_then_should_preserve_operation_identity() {
336 let request = ManagedRequestEnvelope {
337 v: MANAGED_REQUEST_VERSION,
338 operation_id: u128::MAX,
339 payload: vec![4, 3, 2, 1],
340 };
341
342 let bytes = encode_named(&request).expect("managed request encodes");
343 let back: ManagedRequestEnvelope = decode_named(&bytes).expect("managed request decodes");
344
345 assert_eq!(back, request);
346 back.validate().expect("the request is valid");
347 }
348
349 #[test]
350 fn given_a_zero_operation_identity_when_validated_then_should_reject_it() {
351 let request = ManagedRequestEnvelope {
352 v: MANAGED_REQUEST_VERSION,
353 operation_id: 0,
354 payload: Vec::new(),
355 };
356 let command = MutationCommandEnvelope {
357 v: KV_OP_VERSION,
358 operation_id: 0,
359 timestamp_micros: 1,
360 actor_user_id: 7,
361 deployment_id: 3,
362 command_code: AGDX_KV_SET_CODE,
363 scope: None,
364 payload: Vec::new(),
365 };
366
367 assert!(request.validate().is_err());
368 assert!(command.validate().is_err());
369 }
370}