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)]
106#[serde(deny_unknown_fields)]
107pub struct ManagedReadEnvelope {
108 pub v: u32,
109 #[serde(with = "crate::encoding::bin_bytes")]
111 pub read: Vec<u8>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub min_position: Option<MutationPosition>,
114 #[serde(default, skip_serializing_if = "is_false")]
115 pub log_head: bool,
116 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub min_sessions: Vec<SessionPosition>,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct SessionPosition {
128 pub stream_id: u32,
129 pub stream_generation: u64,
131 pub topic: String,
132 pub topic_generation: u64,
134 pub partition_id: u32,
135 pub offset: u64,
137}
138
139#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct AppliedFrontier {
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub kv: Option<MutationPosition>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub fork: Option<MutationPosition>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub graph: Option<MutationPosition>,
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub sessions: Vec<SessionPosition>,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct ManagedReplyEnvelope {
165 pub v: u32,
166 #[serde(default)]
167 pub frontier: AppliedFrontier,
168 #[serde(with = "crate::encoding::bin_bytes")]
169 pub reply: Vec<u8>,
170}
171
172#[expect(clippy::trivially_copy_pass_by_ref, reason = "serde skip predicate")]
173fn is_false(value: &bool) -> bool {
174 !*value
175}
176
177#[derive(Clone, Debug, Serialize, Deserialize)]
182pub struct MutationCommandEnvelope {
183 pub v: u32,
184 pub operation_id: u128,
185 pub deployment_id: u64,
186 pub timestamp_micros: u64,
187 pub command_code: u32,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub scope: Option<crate::forward::ForwardedScope>,
191 #[serde(with = "crate::encoding::bin_bytes")]
192 pub payload: Vec<u8>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub caller: Option<crate::forward::Caller>,
198}
199
200impl MutationCommandEnvelope {
201 pub fn validate(&self) -> Result<(), &'static str> {
202 if self.v == 0 {
203 return Err("mutation command version must not be zero");
204 }
205 if self.operation_id == 0 {
206 return Err("mutation command operation id must not be zero");
207 }
208 Ok(())
209 }
210}
211
212#[cfg(all(test, feature = "cbor"))]
213mod tests {
214 use super::*;
215 use crate::codes::{AGDX_KV_SET_CODE, KV_OP_VERSION};
216 use crate::framing::{decode_named, encode_named};
217
218 #[test]
219 fn given_a_read_envelope_when_round_tripped_then_should_keep_the_inner_read() {
220 let get = crate::kv::KvGet {
221 v: KV_OP_VERSION,
222 namespace: "orders".to_owned(),
223 key: b"k".to_vec(),
224 if_none_match: None,
225 min_position: None,
226 };
227 let read = encode_named(&get).expect("encodes");
228 let envelope = ManagedReadEnvelope {
229 v: MANAGED_REQUEST_VERSION,
230 read: read.clone(),
231 min_position: Some(MutationPosition {
232 topic_generation: 3,
233 partition: 0,
234 offset: 41,
235 }),
236 log_head: true,
237 min_sessions: vec![SessionPosition {
238 stream_id: 2,
239 stream_generation: 10,
240 topic: "agent.sessions".to_owned(),
241 topic_generation: 11,
242 partition_id: 1,
243 offset: 7,
244 }],
245 };
246 let decoded: ManagedReadEnvelope =
247 decode_named(&encode_named(&envelope).expect("encodes")).expect("decodes");
248 assert_eq!(decoded, envelope);
249 assert!(decode_named::<ManagedReadEnvelope>(&read).is_err());
252 let managed = encode_named(&ManagedRequestEnvelope {
253 v: MANAGED_REQUEST_VERSION,
254 operation_id: 9,
255 payload: read,
256 })
257 .expect("encodes");
258 assert!(decode_named::<ManagedReadEnvelope>(&managed).is_err());
259 }
260
261 #[test]
262 fn given_a_reply_envelope_when_round_tripped_then_should_keep_the_reply_and_refuse_plain_replies()
263 {
264 let reply =
265 encode_named(&crate::kv::KvReply::Ok(crate::kv::KvOutcome::Written)).expect("encodes");
266 let envelope = ManagedReplyEnvelope {
267 v: MANAGED_REQUEST_VERSION,
268 frontier: AppliedFrontier {
269 kv: Some(MutationPosition {
270 topic_generation: 1,
271 partition: 0,
272 offset: 7,
273 }),
274 fork: None,
275 graph: None,
276 sessions: Vec::new(),
277 },
278 reply: reply.clone(),
279 };
280 let decoded: ManagedReplyEnvelope =
281 decode_named(&encode_named(&envelope).expect("encodes")).expect("decodes");
282 assert_eq!(decoded, envelope);
283 assert!(decode_named::<ManagedReplyEnvelope>(&reply).is_err());
284 let refusal = encode_named(&crate::result::CommandError::new(
285 crate::result::ResultCode::Stale,
286 "behind",
287 ))
288 .expect("encodes");
289 assert!(decode_named::<ManagedReplyEnvelope>(&refusal).is_err());
290 }
291
292 #[test]
293 fn given_command_codes_when_classified_then_should_name_their_surface() {
294 use crate::codes::{
295 AGDX_FORK_LIST_CODE, AGDX_GRAPH_UPSERT_CODE, AGDX_KV_LEASE_CODE, AGDX_QUERY_CODE,
296 AGDX_SESSION_GET_CODE,
297 };
298 assert_eq!(
299 PositionSurface::of_code(AGDX_KV_LEASE_CODE),
300 Some(PositionSurface::Kv)
301 );
302 assert_eq!(
303 PositionSurface::of_code(AGDX_FORK_LIST_CODE),
304 Some(PositionSurface::Fork)
305 );
306 assert_eq!(
307 PositionSurface::of_code(AGDX_GRAPH_UPSERT_CODE),
308 Some(PositionSurface::Graph)
309 );
310 assert_eq!(PositionSurface::of_code(AGDX_SESSION_GET_CODE), None);
311 assert_eq!(PositionSurface::of_code(AGDX_QUERY_CODE), None);
312 }
313
314 #[test]
315 fn given_a_command_envelope_when_round_tripped_then_should_preserve_request_bytes() {
316 let envelope = MutationCommandEnvelope {
317 v: KV_OP_VERSION,
318 operation_id: 42,
319 deployment_id: 3,
320 timestamp_micros: 1_700_000_000_000_000,
321 command_code: AGDX_KV_SET_CODE,
322 scope: None,
323 payload: vec![0, 1, 2, 255],
324 caller: Some(crate::forward::Caller {
325 user_id: 7,
326 agent: None,
327 on_behalf_of: None,
328 }),
329 };
330 let bytes = encode_named(&envelope).expect("encodes");
331 let back: MutationCommandEnvelope = decode_named(&bytes).expect("decodes");
332 back.validate().expect("the envelope is valid");
333 assert_eq!(back.v, KV_OP_VERSION);
334 assert_eq!(back.operation_id, 42);
335 assert_eq!(back.caller.map(|caller| caller.user_id), Some(7));
336 assert_eq!(back.deployment_id, 3);
337 assert_eq!(back.timestamp_micros, 1_700_000_000_000_000);
338 assert_eq!(back.command_code, AGDX_KV_SET_CODE);
339 assert_eq!(back.payload, vec![0, 1, 2, 255]);
340 }
341
342 #[test]
343 fn given_a_managed_request_when_round_tripped_then_should_preserve_operation_identity() {
344 let request = ManagedRequestEnvelope {
345 v: MANAGED_REQUEST_VERSION,
346 operation_id: u128::MAX,
347 payload: vec![4, 3, 2, 1],
348 };
349
350 let bytes = encode_named(&request).expect("managed request encodes");
351 let back: ManagedRequestEnvelope = decode_named(&bytes).expect("managed request decodes");
352
353 assert_eq!(back, request);
354 back.validate().expect("the request is valid");
355 }
356
357 #[test]
358 fn given_a_zero_operation_identity_when_validated_then_should_reject_it() {
359 let request = ManagedRequestEnvelope {
360 v: MANAGED_REQUEST_VERSION,
361 operation_id: 0,
362 payload: Vec::new(),
363 };
364 let command = MutationCommandEnvelope {
365 v: KV_OP_VERSION,
366 operation_id: 0,
367 timestamp_micros: 1,
368 deployment_id: 3,
369 command_code: AGDX_KV_SET_CODE,
370 scope: None,
371 payload: Vec::new(),
372 caller: None,
373 };
374
375 assert!(request.validate().is_err());
376 assert!(command.validate().is_err());
377 }
378}