The MoQT transport that joule-code-collab's network rung needs lived in the private joulesperbit repo (as inv-moq + jwp), so a public consumer couldn't depend on it. Port both into the public open-standards joule-code workspace, self-contained (concrete dep versions, not workspace-inherited) so a bare git clone builds them: - crates/jwp: Joule Wire Protocol — energy-aware binary framing (CBOR, zstd/lz4, TCP + optional QUIC). Self-contained, external deps only. - crates/joule-moq: MoQT (Media-over-QUIC) wire protocol + JWP signed-receipt extension for CommunicationOS — Publisher/Subscriber/Relay traits, MockFabric (in-process) + a production QUIC path (quic feature). Renamed from inv-moq to drop the invisible-* namespace and match the joule-code branding; depends only on jwp. Both build (default + quic) and pass their tests (joule-moq 29, jwp 152) in the new home. The transport-moqt example now path-deps the public joule-moq instead of reaching into joulesperbit, and its end-to-end BFT-ops-over-MoQT-with-signed- receipts tests pass. This unblocks public consumers (agent-zed) from git-depping the transport for cross-machine fleet coordination. Co-Authored-By: Claude Opus 4.8 <[email protected]>
156 lines
5.3 KiB
Rust
156 lines
5.3 KiB
Rust
//! Pluggable payload encoding for JWP frames.
|
|
//!
|
|
//! The [`PayloadCodec`] trait abstracts payload serialization so the
|
|
//! protocol can swap encodings per-frame based on adaptive decisions.
|
|
|
|
use serde::{Serialize, de::DeserializeOwned};
|
|
|
|
use crate::error::JwpError;
|
|
|
|
// ── Encoding identifier ──────────────────────────────────────────
|
|
|
|
/// Identifies the payload encoding used in a frame.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
#[repr(u8)]
|
|
pub enum EncodingId {
|
|
/// Canonical CBOR (RFC 8949) — the default and only v1 encoding.
|
|
Cbor = 0x01,
|
|
// Future: FlatBuf = 0x02, Raw = 0x03, MsgPack = 0x04
|
|
}
|
|
|
|
impl EncodingId {
|
|
/// Parse from wire byte.
|
|
pub fn from_u8(b: u8) -> Option<Self> {
|
|
match b {
|
|
0x01 => Some(Self::Cbor),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Trait ─────────────────────────────────────────────────────────
|
|
|
|
/// Trait for pluggable payload serialization.
|
|
///
|
|
/// Implementations must be `Send + Sync` so they can be stored in
|
|
/// codecs shared across async tasks.
|
|
pub trait PayloadCodec: Send + Sync {
|
|
/// Serialize a value to wire bytes.
|
|
fn encode_payload<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, JwpError>;
|
|
|
|
/// Deserialize wire bytes back to a value.
|
|
fn decode_payload<T: DeserializeOwned>(&self, data: &[u8]) -> Result<T, JwpError>;
|
|
|
|
/// The encoding identifier for this codec.
|
|
fn encoding_id(&self) -> EncodingId;
|
|
|
|
/// Estimated per-payload overhead in bytes (for adaptive decisions).
|
|
///
|
|
/// This is the typical number of bytes that the encoding format
|
|
/// adds beyond the raw field values. Used by the [`ConnectionProfile`]
|
|
/// to decide when a lighter encoding would save energy.
|
|
fn estimated_overhead_bytes(&self) -> usize;
|
|
}
|
|
|
|
// ── CBOR implementation ──────────────────────────────────────────
|
|
|
|
/// The default CBOR codec — wraps the existing `cbor_encode`/`cbor_decode`.
|
|
///
|
|
/// Uses canonical deterministic CBOR (RFC 8949) via `ciborium`.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct CborPayloadCodec;
|
|
|
|
impl PayloadCodec for CborPayloadCodec {
|
|
fn encode_payload<T: Serialize>(&self, value: &T) -> Result<Vec<u8>, JwpError> {
|
|
crate::frame::cbor_encode(value)
|
|
}
|
|
|
|
fn decode_payload<T: DeserializeOwned>(&self, data: &[u8]) -> Result<T, JwpError> {
|
|
crate::frame::cbor_decode(data)
|
|
}
|
|
|
|
fn encoding_id(&self) -> EncodingId {
|
|
EncodingId::Cbor
|
|
}
|
|
|
|
fn estimated_overhead_bytes(&self) -> usize {
|
|
// CBOR adds ~15-30 bytes of type/length metadata for typical
|
|
// search payloads (map keys, string length prefixes, etc.)
|
|
20
|
|
}
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::frame::{HandshakePayload, QueryPayload, ResultPayload};
|
|
|
|
#[test]
|
|
fn cbor_roundtrip_query_via_trait() {
|
|
let codec = CborPayloadCodec;
|
|
let payload = QueryPayload {
|
|
query: "rust async runtime".into(),
|
|
limit: 25,
|
|
session_id: Some("sess-42".into()),
|
|
};
|
|
|
|
let bytes = codec.encode_payload(&payload).unwrap();
|
|
let decoded: QueryPayload = codec.decode_payload(&bytes).unwrap();
|
|
|
|
assert_eq!(decoded.query, "rust async runtime");
|
|
assert_eq!(decoded.limit, 25);
|
|
assert_eq!(decoded.session_id.as_deref(), Some("sess-42"));
|
|
}
|
|
|
|
#[test]
|
|
fn cbor_roundtrip_result_via_trait() {
|
|
let codec = CborPayloadCodec;
|
|
let payload = ResultPayload {
|
|
rank: 3,
|
|
url: "https://docs.rs/tokio".into(),
|
|
title: "Tokio Docs".into(),
|
|
domain: "docs.rs".into(),
|
|
score: 8.5,
|
|
content_hash: "blake3_abc".into(),
|
|
};
|
|
|
|
let bytes = codec.encode_payload(&payload).unwrap();
|
|
let decoded: ResultPayload = codec.decode_payload(&bytes).unwrap();
|
|
|
|
assert_eq!(decoded.rank, 3);
|
|
assert_eq!(decoded.url, "https://docs.rs/tokio");
|
|
assert_eq!(decoded.domain, "docs.rs");
|
|
}
|
|
|
|
#[test]
|
|
fn cbor_roundtrip_handshake_via_trait() {
|
|
let codec = CborPayloadCodec;
|
|
let payload = HandshakePayload {
|
|
version: 1,
|
|
capabilities: vec!["search".into(), "cancel".into()],
|
|
};
|
|
|
|
let bytes = codec.encode_payload(&payload).unwrap();
|
|
let decoded: HandshakePayload = codec.decode_payload(&bytes).unwrap();
|
|
|
|
assert_eq!(decoded.version, 1);
|
|
assert_eq!(decoded.capabilities, vec!["search", "cancel"]);
|
|
}
|
|
|
|
#[test]
|
|
fn encoding_id_and_overhead() {
|
|
let codec = CborPayloadCodec;
|
|
assert_eq!(codec.encoding_id(), EncodingId::Cbor);
|
|
assert!(codec.estimated_overhead_bytes() > 0);
|
|
assert!(codec.estimated_overhead_bytes() < 100);
|
|
}
|
|
|
|
#[test]
|
|
fn encoding_id_from_u8() {
|
|
assert_eq!(EncodingId::from_u8(0x01), Some(EncodingId::Cbor));
|
|
assert_eq!(EncodingId::from_u8(0x00), None);
|
|
assert_eq!(EncodingId::from_u8(0xFF), None);
|
|
}
|
|
}
|