xenia_wire/frame.rs
1// Copyright (c) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Payload serialization contract and reference types.
5//!
6//! Xenia's wire is payload-agnostic: [`crate::seal`] and [`crate::open`]
7//! work on any type that implements [`Sealable`]. The default
8//! implementation uses `bincode` for its compact binary encoding, but any
9//! `to_bin` / `from_bin` pair is acceptable as long as both peers agree.
10//!
11//! The reference types [`Frame`] and [`Input`] are provided behind the
12//! default `reference-frame` feature so the quick-start example works
13//! without flag wrangling. For real applications you will almost certainly
14//! want to define your own payload structures that carry your domain's
15//! semantics, and implement [`Sealable`] for them.
16
17use crate::WireError;
18
19/// Serialization contract for any payload that can travel over the Xenia
20/// wire.
21///
22/// Implementations should use a compact binary encoding. The default
23/// reference types use `bincode::serialize` / `bincode::deserialize`.
24pub trait Sealable: Sized {
25 /// Serialize `self` to a compact binary payload.
26 fn to_bin(&self) -> Result<Vec<u8>, WireError>;
27 /// Deserialize from a binary payload.
28 fn from_bin(bytes: &[u8]) -> Result<Self, WireError>;
29}
30
31/// Reference forward-path payload: a primary stream carrying an
32/// application-defined byte blob.
33///
34/// This is intentionally minimal — just enough to make the quick-start
35/// example work and to demonstrate the [`Sealable`] contract. Real
36/// applications should define their own payload structure with domain
37/// semantics and implement [`Sealable`] directly.
38#[cfg(feature = "reference-frame")]
39#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40pub struct Frame {
41 /// Monotonically increasing identifier. Convention only — the wire
42 /// does not enforce monotonicity on this field.
43 pub frame_id: u64,
44 /// Timestamp in milliseconds since the Unix epoch. Convention only —
45 /// the wire does not validate this field.
46 pub timestamp_ms: u64,
47 /// Opaque application payload.
48 pub payload: Vec<u8>,
49}
50
51#[cfg(feature = "reference-frame")]
52impl Sealable for Frame {
53 fn to_bin(&self) -> Result<Vec<u8>, WireError> {
54 bincode::serialize(self).map_err(WireError::encode)
55 }
56 fn from_bin(bytes: &[u8]) -> Result<Self, WireError> {
57 bincode::deserialize(bytes).map_err(WireError::decode)
58 }
59}
60
61/// Reference reverse-path payload: an input event batch carrying an
62/// application-defined byte blob.
63///
64/// Paired with [`Frame`] for bidirectional remote-control use. The
65/// sequence field is a convention for caller-side ordering; the wire's
66/// own replay window operates on the AEAD nonce sequence, not on this
67/// field.
68#[cfg(feature = "reference-frame")]
69#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70pub struct Input {
71 /// Caller-side sequence number for ordering events.
72 pub sequence: u64,
73 /// Timestamp in milliseconds.
74 pub timestamp_ms: u64,
75 /// Opaque application payload.
76 pub payload: Vec<u8>,
77}
78
79#[cfg(feature = "reference-frame")]
80impl Sealable for Input {
81 fn to_bin(&self) -> Result<Vec<u8>, WireError> {
82 bincode::serialize(self).map_err(WireError::encode)
83 }
84 fn from_bin(bytes: &[u8]) -> Result<Self, WireError> {
85 bincode::deserialize(bytes).map_err(WireError::decode)
86 }
87}
88
89#[cfg(all(test, feature = "reference-frame"))]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn frame_roundtrip() {
95 let f = Frame {
96 frame_id: 42,
97 timestamp_ms: 1_700_000_000_000,
98 payload: b"hello".to_vec(),
99 };
100 let bytes = f.to_bin().unwrap();
101 let decoded = Frame::from_bin(&bytes).unwrap();
102 assert_eq!(f, decoded);
103 }
104
105 #[test]
106 fn input_roundtrip() {
107 let i = Input {
108 sequence: 7,
109 timestamp_ms: 1_700_000_000_050,
110 payload: b"click".to_vec(),
111 };
112 let bytes = i.to_bin().unwrap();
113 let decoded = Input::from_bin(&bytes).unwrap();
114 assert_eq!(i, decoded);
115 }
116
117 #[test]
118 fn from_bin_rejects_garbage() {
119 assert!(Frame::from_bin(&[0xFFu8; 4]).is_err());
120 }
121}