Skip to main content

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}