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.
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:
let env = pending_envelope(version)
.event_type(name)
.payload(bytes)
.build(metadata); // Option<Bytes>PersistedEnvelope— owned, read-path. Onebytes::Bytesbuffer plus cached offset ranges for each field. It is cheap to clone (anArcincrement, no copy) and carries no lifetime, so it flows straight through afutures::Streamwith no bridging code.
Because the envelope owns a single Bytes buffer, the read path never re-parses
header bytes and never allocates per field.
Serialization is two traits, Encode<E> and Decode<E>:
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<'a> where Self: 'a;
fn decode<'a>(&'a self, env: &'a PersistedEnvelope) -> Result<Self::Output<'a>, Self::Error>;
}Encode returns bytes::Bytes, so the encoded payload flows end to end with no
Vec → Bytes copy.
The clever part is Decode's associated type Output<'a>. It lets one trait
serve two very different kinds of codec:
| Codec | Output<'a> | Cost |
|---|---|---|
| serde (JSON/bincode/postcard) | E | one allocation per event — an owned value |
| rkyv (archived) | &'a Archived<E> | zero copy — borrows into the buffer |
bytemuck (#[repr(C)] POD) | &'a E | zero 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<'a> Decode<E, Output<'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).
Behind cargo features, mnesis-store ships:
serde/json—SerdeCodec<F>, withJsonCodecas theJsonalias. The default for anything serde-encodable.bytemuck—BytemuckCodecfor#[repr(C)]POD types.rkyv—RkyvCodecfor rkyv-archived zero-copy types.
Or implement Encode/Decode yourself for a bespoke format — the store only
ever sees bytes.
- Repository — where the codec is plugged in.
- Subscriptions — decoding a live stream with
.decoded(). - Stability —
bytesis a pinned public dependency.