Skip to main content

statevec_model/
model.rs

1// Copyright 2026 Jumpex Technology.
2// SPDX-License-Identifier: Apache-2.0
3
4use serde::{Deserialize, Serialize};
5use smallvec::SmallVec;
6use std::fmt;
7
8// ---- Record types ----
9
10/// Stable numeric kind assigned to a record type within a schema version.
11pub type RecordKind = u8;
12/// Host-assigned system identifier for one materialized record.
13pub type SysId = u64;
14
15/// Fully qualified record key used by runtime hosts.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct RecordKey {
18    /// Stable record kind.
19    pub kind: RecordKind,
20    /// Host-assigned system identifier.
21    pub sys_id: SysId,
22}
23/// Monotonic committed transaction sequence.
24pub type TxSeq = u64;
25/// Encoded primary-key bytes.
26pub type PkBytes = SmallVec<[u8; 64]>;
27/// Function pointer used by generated record schemas to encode primary keys.
28pub type PkEncodeFn = fn(&[u8]) -> PkBytes;
29
30/// Number of bytes reserved by the runtime record header.
31///
32/// These bytes are host-owned metadata and are not available to generated
33/// record fields. A record with `RECORD_LEN = 64` has `48` bytes of generated
34/// record data.
35pub const RECORD_HEADER_SIZE: usize = 16;
36
37/// Primitive field representation supported by StateVec schemas.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FieldType {
40    /// Boolean field encoded as one byte.
41    Bool,
42    /// Unsigned 8-bit integer.
43    U8,
44    /// Unsigned 16-bit little-endian integer.
45    U16,
46    /// Unsigned 32-bit little-endian integer.
47    U32,
48    /// Unsigned 64-bit little-endian integer.
49    U64,
50    /// Signed 32-bit little-endian integer.
51    I32,
52    /// Signed 64-bit little-endian integer.
53    I64,
54    /// Unsigned 128-bit little-endian integer.
55    U128,
56    /// Fixed-capacity byte field with an encoded logical length.
57    FixedBytes,
58    /// Variable-length byte field used in command and event payloads.
59    VarBytes,
60    /// User-defined `repr(u8)` enum.
61    EnumU8,
62}
63
64/// Two-component schema version.
65#[repr(C)]
66#[derive(
67    Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize,
68)]
69pub struct Version {
70    main: u8,
71    minor: u8,
72}
73
74const _: () = assert!(std::mem::size_of::<Version>() == 2);
75
76impl Version {
77    /// Creates a schema version from main and minor components.
78    #[inline(always)]
79    pub const fn new(main: u8, minor: u8) -> Self {
80        Self { main, minor }
81    }
82
83    /// Returns the main schema version component.
84    #[inline(always)]
85    pub const fn main(self) -> u8 {
86        self.main
87    }
88
89    /// Returns the minor schema version component.
90    #[inline(always)]
91    pub const fn minor(self) -> u8 {
92        self.minor
93    }
94
95    /// Encodes the version as two bytes.
96    #[inline(always)]
97    pub const fn to_bytes(self) -> [u8; 2] {
98        [self.main, self.minor]
99    }
100
101    /// Decodes the version from two bytes.
102    #[inline(always)]
103    pub const fn from_bytes(bytes: [u8; 2]) -> Self {
104        Self::new(bytes[0], bytes[1])
105    }
106}
107
108impl From<u16> for Version {
109    #[inline(always)]
110    fn from(value: u16) -> Self {
111        Self::from_bytes(value.to_le_bytes())
112    }
113}
114
115impl From<Version> for u16 {
116    #[inline(always)]
117    fn from(value: Version) -> Self {
118        u16::from_le_bytes(value.to_bytes())
119    }
120}
121
122/// Static definition for one schema field.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct FieldDefinition {
125    /// Rust/source-level field name.
126    pub name: &'static str,
127    /// Stable field index within the enclosing schema object.
128    pub field_index: u32,
129    /// Byte offset for fixed-layout record fields.
130    pub offset: u32,
131    /// Encoded field type.
132    pub ty: FieldType,
133    /// Encoded byte length for fixed-layout fields.
134    pub len: u32,
135    /// Rust type name recorded for diagnostics and IDL output.
136    pub rust_type_name: &'static str,
137    /// Enum type name when `ty` is [`FieldType::EnumU8`].
138    pub enum_type_name: Option<&'static str>,
139    /// Whether this field is immutable after record creation.
140    pub immutable: bool,
141}
142
143/// Static definition for one record type.
144#[derive(Debug, Clone, Copy)]
145pub struct RecordDefinition {
146    /// Stable record kind.
147    pub kind: RecordKind,
148    /// Rust/source-level record name.
149    pub name: &'static str,
150    /// Whether a primary-key index is defined.
151    pub is_pk_idx: bool,
152    /// Whether range scans are expected for this record's primary-key index.
153    pub support_range_scan: bool,
154    /// Record data bytes excluding host-owned metadata.
155    pub data_size: u32,
156    /// Schema version encoded as a compact `u16`.
157    pub version: u16,
158    /// Optional generated primary-key encoder.
159    pub pk_encode: Option<PkEncodeFn>,
160    /// Active schema fields.
161    pub fields: &'static [FieldDefinition],
162    /// Reserved fields retained for layout compatibility.
163    pub reserved_fields: &'static [FieldDefinition],
164    /// Field names used to build the primary key.
165    pub pk_fields: &'static [&'static str],
166}
167
168impl RecordDefinition {
169    /// Returns the active field definition with the given source-level name.
170    #[inline]
171    pub fn field_by_name(&self, name: &str) -> Option<&FieldDefinition> {
172        self.fields.iter().find(|f| f.name == name)
173    }
174}
175
176/// Trait implemented by generated record types.
177pub trait RecordSchema {
178    /// Stable record kind.
179    const KIND: RecordKind;
180    /// Fixed record byte length.
181    const RECORD_LEN: usize;
182    /// Number of active fields.
183    const FIELD_COUNT: usize;
184
185    /// Returns the static record definition.
186    fn definition() -> &'static RecordDefinition;
187}
188
189/// Primary-key encoding hook implemented by generated record types.
190pub trait PkCodec {
191    /// Encodes primary-key bytes from a fixed-layout record buffer.
192    fn encode_pk_from_bytes(data: &[u8]) -> PkBytes;
193}
194
195/// Generated typed accessors for a fixed-layout record.
196pub trait GeneratedRecordAccess: RecordSchema {
197    /// Record data bytes excluding host-owned metadata.
198    const DATA_LEN: usize;
199    /// Read-only accessor type.
200    type Access<'a>;
201    /// Builder used when creating a new record.
202    type NewBuilder<'a>;
203    /// Builder used when updating an existing record.
204    type UpdateBuilder<'a>;
205    /// Wraps a record buffer in a read-only accessor.
206    fn wrap<'a>(buf: &'a [u8]) -> Self::Access<'a>;
207    /// Wraps a record buffer in a creation builder.
208    fn wrap_new<'a>(buf: &'a mut [u8]) -> Self::NewBuilder<'a>;
209    /// Wraps a record buffer in an update builder.
210    fn wrap_update<'a>(buf: &'a mut [u8]) -> Self::UpdateBuilder<'a>;
211}
212
213/// Error returned when an encoded enum discriminant is unknown.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct EnumDecodeError {
216    /// Enum type name.
217    pub type_name: &'static str,
218    /// Raw encoded discriminant.
219    pub raw: u8,
220}
221
222impl fmt::Display for EnumDecodeError {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "invalid {} discriminant: {}", self.type_name, self.raw)
225    }
226}
227
228impl std::error::Error for EnumDecodeError {}
229
230/// Buffer access error used by generated readers.
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub struct AccessError {
233    /// Required number of bytes.
234    pub required: usize,
235    /// Actual available number of bytes.
236    pub actual: usize,
237}
238
239impl fmt::Display for AccessError {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        write!(
242            f,
243            "buffer too small: required {} bytes, got {}",
244            self.required, self.actual
245        )
246    }
247}
248
249impl std::error::Error for AccessError {}
250
251/// Static definition for one `repr(u8)` enum variant.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct EnumVariantDefinition {
254    /// Variant name.
255    pub name: &'static str,
256    /// Encoded discriminant.
257    pub discriminant: u8,
258}
259
260/// Static definition for one generated `repr(u8)` enum.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub struct EnumDefinition {
263    /// Enum type name.
264    pub name: &'static str,
265    /// Variant definitions in declaration order.
266    pub variants: &'static [EnumVariantDefinition],
267}
268
269/// Trait implemented by `#[derive(EnumU8)]`.
270pub trait EnumU8: Copy + Eq + 'static {
271    /// Encodes the enum as its `u8` discriminant.
272    fn to_u8(self) -> u8;
273    /// Decodes the enum from a `u8` discriminant.
274    fn try_from_u8(v: u8) -> Result<Self, EnumDecodeError>;
275    /// Returns the enum type name.
276    fn type_name() -> &'static str;
277    /// Returns the static enum definition.
278    fn definition() -> &'static EnumDefinition;
279}
280
281/// Fixed-capacity byte value with a logical length.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub struct FixedBytes<const N: usize> {
284    len: u16,
285    buf: [u8; N],
286}
287
288impl<const N: usize> FixedBytes<N> {
289    /// Creates a fixed-capacity byte value from a slice.
290    pub fn new(bytes: &[u8]) -> Result<Self, AccessError> {
291        if bytes.len() > N {
292            return Err(AccessError {
293                required: N,
294                actual: bytes.len(),
295            });
296        }
297        let mut buf = [0u8; N];
298        buf[..bytes.len()].copy_from_slice(bytes);
299        Ok(Self {
300            len: bytes.len() as u16,
301            buf,
302        })
303    }
304
305    /// Returns the logical byte length.
306    #[inline(always)]
307    pub fn len(&self) -> usize {
308        self.len as usize
309    }
310
311    /// Returns whether the logical byte content is empty.
312    #[inline(always)]
313    pub fn is_empty(&self) -> bool {
314        self.len == 0
315    }
316
317    /// Returns the logical byte content.
318    #[inline(always)]
319    pub fn as_slice(&self) -> &[u8] {
320        &self.buf[..self.len()]
321    }
322
323    /// Returns the full padded storage buffer.
324    #[inline(always)]
325    pub fn padded_slice(&self) -> &[u8; N] {
326        &self.buf
327    }
328
329    /// Returns fixed-width bytes suitable for primary-key encoding.
330    #[inline]
331    pub fn pk_bytes(&self) -> PkBytes {
332        let mut pk = PkBytes::new();
333        pk.extend_from_slice(&self.buf);
334        pk
335    }
336}
337
338impl<const N: usize, const M: usize> From<&[u8; M]> for FixedBytes<N> {
339    #[inline]
340    fn from(bytes: &[u8; M]) -> Self {
341        const { assert!(M <= N, "FixedBytes: source length exceeds capacity") };
342        let mut buf = [0u8; N];
343        buf[..M].copy_from_slice(bytes);
344        Self { len: M as u16, buf }
345    }
346}
347
348/// Incremental primary-key byte encoder.
349pub struct PkBuilder {
350    buf: PkBytes,
351}
352
353impl PkBuilder {
354    /// Creates an empty primary-key builder.
355    #[inline]
356    pub fn new() -> Self {
357        Self {
358            buf: SmallVec::new(),
359        }
360    }
361
362    /// Appends a `u8` component.
363    #[inline]
364    pub fn push_u8(&mut self, v: u8) {
365        self.buf.push(v);
366    }
367
368    /// Appends a big-endian `u16` component.
369    #[inline]
370    pub fn push_u16(&mut self, v: u16) {
371        self.buf.extend_from_slice(&v.to_be_bytes());
372    }
373
374    /// Appends a big-endian `u32` component.
375    #[inline]
376    pub fn push_u32(&mut self, v: u32) {
377        self.buf.extend_from_slice(&v.to_be_bytes());
378    }
379
380    /// Appends a big-endian `u64` component.
381    #[inline]
382    pub fn push_u64(&mut self, v: u64) {
383        self.buf.extend_from_slice(&v.to_be_bytes());
384    }
385
386    /// Appends an order-preserving signed `i32` component.
387    #[inline]
388    pub fn push_i32(&mut self, v: i32) {
389        let encoded = ((v as u32) ^ 0x8000_0000).to_be_bytes();
390        self.buf.extend_from_slice(&encoded);
391    }
392
393    /// Appends an order-preserving signed `i64` component.
394    #[inline]
395    pub fn push_i64(&mut self, v: i64) {
396        let encoded = ((v as u64) ^ 0x8000_0000_0000_0000).to_be_bytes();
397        self.buf.extend_from_slice(&encoded);
398    }
399
400    /// Appends raw bytes.
401    #[inline]
402    pub fn push_bytes(&mut self, bytes: &[u8]) {
403        self.buf.extend_from_slice(bytes);
404    }
405
406    /// Finishes primary-key encoding.
407    #[inline]
408    pub fn finish(self) -> PkBytes {
409        self.buf
410    }
411}
412
413impl Default for PkBuilder {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419/// Returns `Err` if `buf` is shorter than `need` bytes.
420#[inline(always)]
421fn check_len(buf: &[u8], need: usize) -> Result<(), AccessError> {
422    if buf.len() >= need {
423        Ok(())
424    } else {
425        Err(AccessError {
426            required: need,
427            actual: buf.len(),
428        })
429    }
430}
431
432#[inline(always)]
433fn checked_need(offset: usize, len: usize, actual: usize) -> Result<usize, AccessError> {
434    offset.checked_add(len).ok_or(AccessError {
435        required: usize::MAX,
436        actual,
437    })
438}
439
440/// Panics if `buf` is shorter than `need` bytes. Used only by write primitives
441/// whose callers guarantee the buffer is framework-sized.
442#[inline(always)]
443fn assert_len(buf: &[u8], need: usize) {
444    assert!(
445        buf.len() >= need,
446        "write buffer too small: {} < {}",
447        buf.len(),
448        need
449    );
450}
451
452#[inline(always)]
453/// Reads a `u8` from a fixed-layout buffer.
454pub fn read_u8(buf: &[u8], offset: usize) -> Result<u8, AccessError> {
455    check_len(buf, checked_need(offset, 1, buf.len())?)?;
456    Ok(buf[offset])
457}
458
459#[inline(always)]
460/// Writes a `u8` into a fixed-layout buffer.
461pub fn write_u8(buf: &mut [u8], offset: usize, v: u8) {
462    assert_len(buf, offset + 1);
463    buf[offset] = v;
464}
465
466#[inline(always)]
467/// Reads a boolean from a fixed-layout buffer.
468pub fn read_bool(buf: &[u8], offset: usize) -> Result<bool, AccessError> {
469    Ok(read_u8(buf, offset)? != 0)
470}
471
472#[inline(always)]
473/// Writes a boolean into a fixed-layout buffer.
474pub fn write_bool(buf: &mut [u8], offset: usize, v: bool) {
475    write_u8(buf, offset, if v { 1 } else { 0 });
476}
477
478#[inline(always)]
479/// Reads a little-endian `u16` from a fixed-layout buffer.
480pub fn read_u16_le(buf: &[u8], offset: usize) -> Result<u16, AccessError> {
481    let end = checked_need(offset, 2, buf.len())?;
482    check_len(buf, end)?;
483    let mut tmp = [0u8; 2];
484    tmp.copy_from_slice(&buf[offset..end]);
485    Ok(u16::from_le_bytes(tmp))
486}
487
488#[inline(always)]
489/// Writes a little-endian `u16` into a fixed-layout buffer.
490pub fn write_u16_le(buf: &mut [u8], offset: usize, v: u16) {
491    assert_len(buf, offset + 2);
492    buf[offset..offset + 2].copy_from_slice(&v.to_le_bytes());
493}
494
495#[inline(always)]
496/// Reads a little-endian `u32` from a fixed-layout buffer.
497pub fn read_u32_le(buf: &[u8], offset: usize) -> Result<u32, AccessError> {
498    let end = checked_need(offset, 4, buf.len())?;
499    check_len(buf, end)?;
500    let mut tmp = [0u8; 4];
501    tmp.copy_from_slice(&buf[offset..end]);
502    Ok(u32::from_le_bytes(tmp))
503}
504
505#[inline(always)]
506/// Writes a little-endian `u32` into a fixed-layout buffer.
507pub fn write_u32_le(buf: &mut [u8], offset: usize, v: u32) {
508    assert_len(buf, offset + 4);
509    buf[offset..offset + 4].copy_from_slice(&v.to_le_bytes());
510}
511
512#[inline(always)]
513/// Reads a little-endian `u64` from a fixed-layout buffer.
514pub fn read_u64_le(buf: &[u8], offset: usize) -> Result<u64, AccessError> {
515    let end = checked_need(offset, 8, buf.len())?;
516    check_len(buf, end)?;
517    let mut tmp = [0u8; 8];
518    tmp.copy_from_slice(&buf[offset..end]);
519    Ok(u64::from_le_bytes(tmp))
520}
521
522#[inline(always)]
523/// Writes a little-endian `u64` into a fixed-layout buffer.
524pub fn write_u64_le(buf: &mut [u8], offset: usize, v: u64) {
525    assert_len(buf, offset + 8);
526    buf[offset..offset + 8].copy_from_slice(&v.to_le_bytes());
527}
528
529#[inline(always)]
530/// Reads a little-endian `i32` from a fixed-layout buffer.
531pub fn read_i32_le(buf: &[u8], offset: usize) -> Result<i32, AccessError> {
532    Ok(read_u32_le(buf, offset)? as i32)
533}
534
535#[inline(always)]
536/// Writes a little-endian `i32` into a fixed-layout buffer.
537pub fn write_i32_le(buf: &mut [u8], offset: usize, v: i32) {
538    write_u32_le(buf, offset, v as u32);
539}
540
541#[inline(always)]
542/// Reads a little-endian `i64` from a fixed-layout buffer.
543pub fn read_i64_le(buf: &[u8], offset: usize) -> Result<i64, AccessError> {
544    Ok(read_u64_le(buf, offset)? as i64)
545}
546
547#[inline(always)]
548/// Writes a little-endian `i64` into a fixed-layout buffer.
549pub fn write_i64_le(buf: &mut [u8], offset: usize, v: i64) {
550    write_u64_le(buf, offset, v as u64);
551}
552
553#[inline(always)]
554/// Reads a little-endian `u128` from a fixed-layout buffer.
555pub fn read_u128_le(buf: &[u8], offset: usize) -> Result<u128, AccessError> {
556    let end = checked_need(offset, 16, buf.len())?;
557    check_len(buf, end)?;
558    let mut tmp = [0u8; 16];
559    tmp.copy_from_slice(&buf[offset..end]);
560    Ok(u128::from_le_bytes(tmp))
561}
562
563#[inline(always)]
564/// Writes a little-endian `u128` into a fixed-layout buffer.
565pub fn write_u128_le(buf: &mut [u8], offset: usize, v: u128) {
566    assert_len(buf, offset + 16);
567    buf[offset..offset + 16].copy_from_slice(&v.to_le_bytes());
568}
569
570#[inline(always)]
571/// Reads a fixed-capacity byte field from a fixed-layout buffer.
572pub fn read_fixed_bytes<const N: usize>(
573    buf: &[u8],
574    offset: usize,
575) -> Result<FixedBytes<N>, AccessError> {
576    let start = checked_need(offset, 2, buf.len())?;
577    let end = checked_need(start, N, buf.len())?;
578    check_len(buf, end)?;
579    let len = read_u16_le(buf, offset)? as usize;
580    if len > N {
581        return Err(AccessError {
582            required: len,
583            actual: N,
584        });
585    }
586    let mut tmp = [0u8; N];
587    tmp.copy_from_slice(&buf[start..end]);
588    Ok(FixedBytes {
589        len: len as u16,
590        buf: tmp,
591    })
592}
593
594/// Writes a fixed-capacity byte field into a fixed-layout buffer.
595///
596/// The caller must provide a record data buffer large enough for the field
597/// offset and encoded size. Generated record builders pass buffers with
598/// `R::DATA_LEN` bytes.
599#[inline(always)]
600pub fn write_fixed_bytes<const N: usize>(buf: &mut [u8], offset: usize, v: &FixedBytes<N>) {
601    assert_len(buf, offset + 2 + N);
602    write_u16_le(buf, offset, v.len);
603    buf[offset + 2..offset + 2 + N].fill(0);
604    buf[offset + 2..offset + 2 + v.len()].copy_from_slice(v.as_slice());
605}
606
607// ---- Command types ----
608
609/// Runtime command value with kind, external sequence, external time, and payload.
610#[derive(Debug, Clone, PartialEq, Eq)]
611pub struct Command {
612    command_kind: u8,
613    ext_seq: u64,
614    ref_ext_time_us: u64,
615    payload: Vec<u8>,
616}
617
618impl Command {
619    /// Creates a command envelope.
620    #[inline]
621    pub fn new(command_kind: u8, ext_seq: u64, ref_ext_time_us: u64, payload: Vec<u8>) -> Self {
622        Self {
623            command_kind,
624            ext_seq,
625            ref_ext_time_us,
626            payload,
627        }
628    }
629
630    /// Returns the command kind.
631    #[inline(always)]
632    pub fn command_kind(&self) -> u8 {
633        self.command_kind
634    }
635
636    /// Returns the source queue sequence.
637    #[inline(always)]
638    pub fn ext_seq(&self) -> u64 {
639        self.ext_seq
640    }
641
642    /// Returns the dedupe key used by ingress.
643    #[inline(always)]
644    pub fn ingress_dedupe_key(&self) -> u64 {
645        self.ext_seq
646    }
647
648    /// Returns the source-provided reference time in microseconds.
649    #[inline(always)]
650    pub fn ref_ext_time_us(&self) -> u64 {
651        self.ref_ext_time_us
652    }
653
654    /// Returns the payload byte length.
655    #[inline(always)]
656    pub fn payload_len(&self) -> usize {
657        self.payload.len()
658    }
659
660    /// Returns the encoded command payload.
661    #[inline(always)]
662    pub fn payload(&self) -> &[u8] {
663        &self.payload
664    }
665
666    /// Consumes the command and returns the encoded payload.
667    #[inline(always)]
668    pub fn into_payload(self) -> Vec<u8> {
669        self.payload
670    }
671}
672
673// ---- VarBytes helpers (payload-layer only) ----
674
675#[inline(always)]
676/// Reads a payload-layer variable byte field and returns the field plus next offset.
677pub fn read_var_bytes(data: &[u8], offset: usize) -> Result<(&[u8], usize), AccessError> {
678    let len = read_u16_le(data, offset)? as usize;
679    let end = checked_need(checked_need(offset, 2, data.len())?, len, data.len())?;
680    if end > data.len() {
681        return Err(AccessError {
682            required: end,
683            actual: data.len(),
684        });
685    }
686    Ok((&data[offset + 2..end], end))
687}
688
689#[inline]
690/// Appends a payload-layer variable byte field.
691pub fn write_var_bytes(buf: &mut Vec<u8>, content: &[u8]) -> Result<(), AccessError> {
692    let len = u16::try_from(content.len()).map_err(|_| AccessError {
693        required: content.len(),
694        actual: u16::MAX as usize,
695    })?;
696    buf.extend_from_slice(&len.to_le_bytes());
697    buf.extend_from_slice(content);
698    Ok(())
699}
700
701// ---- Payload schema types ----
702
703/// Static definition for one command or event payload field.
704#[derive(Debug, Clone, Copy)]
705pub struct PayloadFieldDefinition {
706    /// Rust/source-level field name.
707    pub name: &'static str,
708    /// Stable field index within the payload.
709    pub field_index: u32,
710    /// Encoded field type.
711    pub ty: FieldType,
712    /// Rust type name recorded for diagnostics and IDL output.
713    pub rust_type_name: &'static str,
714    /// Enum type name when `ty` is [`FieldType::EnumU8`].
715    pub enum_type_name: Option<&'static str>,
716    /// Fixed encoded size for fixed-width payload fields.
717    pub fixed_size: Option<u32>,
718}
719
720/// Static definition for one command type.
721#[derive(Debug, Clone, Copy)]
722pub struct CommandDefinition {
723    /// Stable command kind.
724    pub kind: u8,
725    /// Rust/source-level command name.
726    pub name: &'static str,
727    /// Schema version encoded as a compact `u16`.
728    pub version: u16,
729    /// Payload fields.
730    pub fields: &'static [PayloadFieldDefinition],
731}
732
733/// Trait implemented by generated command types.
734pub trait CommandSchema {
735    /// Stable command kind.
736    const KIND: u8;
737    /// Returns the static command definition.
738    fn definition() -> &'static CommandDefinition;
739}
740
741/// Generated typed accessors for command payloads.
742pub trait GeneratedCommandAccess: CommandSchema {
743    /// Read-only payload accessor type.
744    type Access<'a>;
745    /// Payload builder type.
746    type Builder;
747    /// Wraps encoded payload bytes in a read-only accessor.
748    fn wrap(data: &[u8]) -> Self::Access<'_>;
749    /// Creates a payload builder.
750    fn builder() -> Self::Builder;
751}
752
753// ---- Event types ----
754
755/// Runtime event value emitted by a command transaction.
756#[derive(Debug, Clone, PartialEq, Eq)]
757pub struct Event {
758    event_kind: u8,
759    event_seq: u32,
760    payload: Vec<u8>,
761}
762
763impl Event {
764    /// Creates an event.
765    #[inline]
766    pub fn new(event_kind: u8, event_seq: u32, payload: Vec<u8>) -> Self {
767        Self {
768            event_kind,
769            event_seq,
770            payload,
771        }
772    }
773
774    /// Returns the event kind.
775    #[inline(always)]
776    pub fn event_kind(&self) -> u8 {
777        self.event_kind
778    }
779
780    /// Returns the event sequence within the transaction.
781    #[inline(always)]
782    pub fn event_seq(&self) -> u32 {
783        self.event_seq
784    }
785
786    /// Returns the encoded event payload.
787    #[inline(always)]
788    pub fn payload(&self) -> &[u8] {
789        &self.payload
790    }
791
792    /// Consumes the event and returns the encoded payload.
793    #[inline(always)]
794    pub fn into_payload(self) -> Vec<u8> {
795        self.payload
796    }
797
798    /// Attaches a committed transaction sequence to this event.
799    #[inline]
800    pub fn into_frame(self, tx_seq: TxSeq) -> EventFrame {
801        EventFrame::new(tx_seq, self.event_seq, self.event_kind, self.payload)
802    }
803}
804
805/// Event plus committed transaction sequence.
806#[derive(Debug, Clone, PartialEq, Eq)]
807pub struct EventFrame {
808    tx_seq: TxSeq,
809    event_seq: u32,
810    event_kind: u8,
811    payload: Vec<u8>,
812}
813
814impl EventFrame {
815    /// Creates an event frame.
816    #[inline]
817    pub fn new(tx_seq: u64, event_seq: u32, event_kind: u8, payload: Vec<u8>) -> Self {
818        Self {
819            tx_seq,
820            event_seq,
821            event_kind,
822            payload,
823        }
824    }
825
826    /// Returns the committed transaction sequence.
827    #[inline(always)]
828    pub fn tx_seq(&self) -> TxSeq {
829        self.tx_seq
830    }
831
832    /// Returns the event sequence within the transaction.
833    #[inline(always)]
834    pub fn event_seq(&self) -> u32 {
835        self.event_seq
836    }
837
838    /// Returns the event kind.
839    #[inline(always)]
840    pub fn event_kind(&self) -> u8 {
841        self.event_kind
842    }
843
844    /// Returns the encoded frame byte length.
845    #[inline(always)]
846    pub fn frame_len(&self) -> usize {
847        24 + self.payload.len()
848    }
849
850    /// Returns the encoded event payload.
851    #[inline(always)]
852    pub fn payload(&self) -> &[u8] {
853        &self.payload
854    }
855
856    /// Consumes the frame and returns the encoded payload.
857    #[inline(always)]
858    pub fn into_payload(self) -> Vec<u8> {
859        self.payload
860    }
861
862    /// Splits the frame into transaction sequence and event.
863    #[inline]
864    pub fn into_parts(self) -> (TxSeq, Event) {
865        (
866            self.tx_seq,
867            Event {
868                event_kind: self.event_kind,
869                event_seq: self.event_seq,
870                payload: self.payload,
871            },
872        )
873    }
874}
875
876/// Static definition for one event type.
877#[derive(Debug, Clone, Copy)]
878pub struct EventDefinition {
879    /// Stable event kind.
880    pub kind: u8,
881    /// Rust/source-level event name.
882    pub name: &'static str,
883    /// Schema version encoded as a compact `u16`.
884    pub version: u16,
885    /// Payload fields.
886    pub fields: &'static [PayloadFieldDefinition],
887}
888
889/// Trait implemented by generated event types.
890pub trait EventSchema {
891    /// Stable event kind.
892    const KIND: u8;
893    /// Returns the static event definition.
894    fn definition() -> &'static EventDefinition;
895}
896
897/// Generated typed accessors for event payloads.
898pub trait GeneratedEventAccess: EventSchema {
899    /// Read-only payload accessor type.
900    type Access<'a>;
901    /// Payload builder type.
902    type Builder;
903    /// Wraps encoded payload bytes in a read-only accessor.
904    fn wrap(data: &[u8]) -> Self::Access<'_>;
905    /// Creates a payload builder.
906    fn builder() -> Self::Builder;
907}