open-standards/joule-code/joule-code-rs/crates/jwp/src/codec.rs
Transaction Science 08c499f8ca joule-code: port CommunicationOS transport (jwp + joule-moq) into the public standard
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]>
2026-06-13 21:29:05 -04:00

191 lines
5.7 KiB
Rust

use bytes::{Buf, BufMut, BytesMut};
use tokio_util::codec::{Decoder, Encoder};
use crate::error::{JwpError, MAX_PAYLOAD_LEN};
use crate::frame::{FrameHeader, HEADER_LEN, JwpFrame};
/// tokio-util codec for JWP frames.
///
/// Reads/writes the 21-byte header followed by the variable-length payload.
pub struct JwpCodec;
impl JwpCodec {
pub fn new() -> Self {
Self
}
}
impl Default for JwpCodec {
fn default() -> Self {
Self::new()
}
}
impl Decoder for JwpCodec {
type Item = JwpFrame;
type Error = JwpError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
// Need at least the header
if src.len() < HEADER_LEN {
return Ok(None);
}
// Peek at header to get payload length (don't consume yet)
let header_bytes: [u8; HEADER_LEN] = src[..HEADER_LEN]
.try_into()
.expect("slice is exactly HEADER_LEN");
let header = FrameHeader::decode(&header_bytes)?;
let payload_len = header.payload_length as usize;
if payload_len > MAX_PAYLOAD_LEN {
return Err(JwpError::PayloadTooLarge(payload_len));
}
let total_len = HEADER_LEN + payload_len;
if src.len() < total_len {
// Reserve space for the full frame so tokio reads enough
src.reserve(total_len - src.len());
return Ok(None);
}
// Consume header
src.advance(HEADER_LEN);
// Consume payload
let payload = src[..payload_len].to_vec();
src.advance(payload_len);
Ok(Some(JwpFrame { header, payload }))
}
}
impl Encoder<JwpFrame> for JwpCodec {
type Error = JwpError;
fn encode(&mut self, item: JwpFrame, dst: &mut BytesMut) -> Result<(), Self::Error> {
if item.payload.len() > MAX_PAYLOAD_LEN {
return Err(JwpError::PayloadTooLarge(item.payload.len()));
}
dst.reserve(HEADER_LEN + item.payload.len());
let mut header_buf = [0u8; HEADER_LEN];
item.header.encode(&mut header_buf);
dst.put_slice(&header_buf);
dst.put_slice(&item.payload);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::{FrameFlags, FrameType, PROTOCOL_VERSION};
fn make_frame(frame_type: FrameType, seq: u32, energy: u64, payload: Vec<u8>) -> JwpFrame {
JwpFrame {
header: FrameHeader {
version: PROTOCOL_VERSION,
frame_type,
payload_length: payload.len() as u32,
energy_uwh: energy,
sequence: seq,
flags: FrameFlags::new(),
},
payload,
}
}
#[test]
fn codec_roundtrip_empty_payload() {
let mut codec = JwpCodec::new();
let frame = make_frame(FrameType::Heartbeat, 1, 0, vec![]);
let mut buf = BytesMut::new();
codec.encode(frame.clone(), &mut buf).unwrap();
let decoded = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.header.frame_type, FrameType::Heartbeat);
assert_eq!(decoded.header.sequence, 1);
assert!(decoded.payload.is_empty());
}
#[test]
fn codec_roundtrip_with_payload() {
let mut codec = JwpCodec::new();
let payload = vec![0xDE, 0xAD, 0xBE, 0xEF];
let frame = make_frame(FrameType::Result, 42, 1500, payload.clone());
let mut buf = BytesMut::new();
codec.encode(frame, &mut buf).unwrap();
let decoded = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(decoded.header.frame_type, FrameType::Result);
assert_eq!(decoded.header.sequence, 42);
assert_eq!(decoded.header.energy_uwh, 1500);
assert_eq!(decoded.payload, payload);
}
#[test]
fn codec_handles_partial_reads() {
let mut codec = JwpCodec::new();
let payload = b"hello world".to_vec();
let frame = make_frame(FrameType::Query, 1, 100, payload);
let mut buf = BytesMut::new();
codec.encode(frame, &mut buf).unwrap();
// Split into two halves
let full = buf.split();
let half1 = &full[..10];
let half2 = &full[10..];
let mut recv_buf = BytesMut::new();
recv_buf.extend_from_slice(half1);
// First decode: not enough data
assert!(codec.decode(&mut recv_buf).unwrap().is_none());
// Feed the rest
recv_buf.extend_from_slice(half2);
let decoded = codec.decode(&mut recv_buf).unwrap().unwrap();
assert_eq!(decoded.header.frame_type, FrameType::Query);
assert_eq!(decoded.payload, b"hello world");
}
#[test]
fn codec_multiple_frames() {
let mut codec = JwpCodec::new();
let mut buf = BytesMut::new();
// Encode two frames back-to-back
let f1 = make_frame(FrameType::Meta, 1, 100, vec![0x01]);
let f2 = make_frame(FrameType::Result, 2, 200, vec![0x02, 0x03]);
codec.encode(f1, &mut buf).unwrap();
codec.encode(f2, &mut buf).unwrap();
// Decode both
let d1 = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(d1.header.sequence, 1);
assert_eq!(d1.payload, vec![0x01]);
let d2 = codec.decode(&mut buf).unwrap().unwrap();
assert_eq!(d2.header.sequence, 2);
assert_eq!(d2.payload, vec![0x02, 0x03]);
// No more frames
assert!(codec.decode(&mut buf).unwrap().is_none());
}
#[test]
fn codec_rejects_oversized_payload() {
let mut codec = JwpCodec::new();
let payload = vec![0u8; MAX_PAYLOAD_LEN + 1];
let frame = make_frame(FrameType::Result, 1, 0, payload);
let mut buf = BytesMut::new();
assert!(codec.encode(frame, &mut buf).is_err());
}
}