Envelopes & Codecs

PendingEnvelope / PersistedEnvelope and the Encode/Decode traits, including the zero-copy Output<'a> GAT.

Envelopes & Codecs

The kernel deals in typed events; a store deals in bytes. The boundary between them is mnesis-store, and it has two jobs: wrap an event with its metadata (the envelope) and serialize its payload (the codec). Keeping these separate is what lets you pick any serialization format without the store caring.

Envelopes — the event's container on the wire

An envelope carries the event payload plus the metadata the store needs: the version, the event-type name, optional user metadata, and the schema version. There are two, one per direction:

PendingEnvelope — owned, write-path. Built with a typestate builder so you cannot forget a field:

rust
let env = pending_envelope(version)
    .event_type(name)
    .payload(bytes)
    .build(metadata);   // Option<Bytes>
  • PersistedEnvelope — owned, read-path. One bytes::Bytes buffer plus cached offset ranges for each field. It is cheap to clone (an Arc increment, no copy) and carries no lifetime, so it flows straight through a futures::Stream with no bridging code.

Because the envelope owns a single Bytes buffer, the read path never re-parses header bytes and never allocates per field.

Codecs — Encode and Decode

Serialization is two traits, Encode<E> and Decode<E>:

rust
pub trait Encode<E: ?Sized>: Send + Sync + 'static {
    fn encode(&self, event: &E) -> Result<bytes::Bytes, Self::Error>;
}

pub trait Decode<E: ?Sized>: Send + Sync + 'static {
    type Output&lt;'a> where Self: 'a;
    fn decode&lt;'a>(&'a self, env: &'a PersistedEnvelope) -> Result<Self::Output&lt;'a>, Self::Error>;
}

Encode returns bytes::Bytes, so the encoded payload flows end to end with no Vec → Bytes copy.

The Output&lt;'a> GAT — one trait for owning and zero-copy codecs

The clever part is Decode's associated type Output&lt;'a>. It lets one trait serve two very different kinds of codec:

CodecOutput&lt;'a>Cost
serde (JSON/bincode/postcard)Eone allocation per event — an owned value
rkyv (archived)&'a Archived<E>zero copy — borrows into the buffer
bytemuck (#[repr(C)] POD)&'a Ezero copy — borrows into the buffer

A zero-copy codec hands you a reference into the envelope's buffer — no deserialization step at all. This is why the on-disk frame aligns every payload to a 16-byte boundary: so &Archived<E> and &repr(C) references are soundly aligned out of the box.

OwningCodec<E> is a convenience alias — for&lt;'a> Decode<E, Output&lt;'a> = E> — naming the owning-codec case so a generic caller writes C: OwningCodec<E> instead of spelling the HRTB. Some carry-away APIs (.decoded(), snapshot decode) require it; the repository façade does not (it consumes the decoded value in place, so it accepts both kinds).

Built-in codecs

Behind cargo features, mnesis-store ships:

  • serde / jsonSerdeCodec<F>, with JsonCodec as the Json alias. The default for anything serde-encodable.
  • bytemuckBytemuckCodec for #[repr(C)] POD types.
  • rkyvRkyvCodec for rkyv-archived zero-copy types.

Or implement Encode/Decode yourself for a bespoke format — the store only ever sees bytes.

Where to next