Split interface store into modules.

This commit is contained in:
Michael Mikovsky
2026-05-31 12:21:33 -06:00
parent 43a84c46f7
commit ba3a419bb2
8 changed files with 635 additions and 1209 deletions
+76
View File
@@ -0,0 +1,76 @@
use crate::protocol::{EndpointError, HookID, Packet, SessionStatus};
/// Ordered event stored by [`crate::interface::InterfaceStore`].
///
/// Events are append-only. Views store indices into this list instead of copying the
/// same packet-flow records into every renderable bucket.
pub struct InterfaceEvent {
/// Monotonic event sequence assigned by the interface store.
pub sequence: u64,
/// Caller-provided timestamp, if the frontend supplied one.
pub time_ns: Option<u64>,
/// Leaf id that emitted or handled the event.
pub leaf_id: u32,
/// Detailed event payload.
pub kind: InterfaceEventKind,
}
/// Interface-visible event emitted by generated helpers.
pub enum InterfaceEventKind {
/// A packet was delivered to a generated leaf.
Inbound { packet: Packet },
/// A packet was queued into an already-live session inbox.
SessionPacketQueued { procedure_id: u32, hook_id: HookID },
/// A hook-backed session was created successfully.
SessionCreated {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// A packet could not create a new session.
SessionRejected {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// One live session received an update tick.
SessionUpdated {
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// One one-shot procedure handler ran.
ProcedureCalled {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
/// A packet was emitted by leaf logic before route retry handling.
OutboundQueued { packet: Packet },
/// A queued outbound packet is about to enter endpoint routing.
RouteAttempt { packet: Packet },
/// Endpoint routing accepted a queued outbound packet.
RouteSuccess { packet: Packet },
/// Endpoint routing rejected a queued outbound packet.
RouteFailure {
packet: Packet,
error: EndpointError,
},
}
+24
View File
@@ -0,0 +1,24 @@
use crate::protocol::HookID;
/// Stable identity for one generated session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SessionKey {
/// Leaf id that owns the generated session family.
pub leaf_id: u32,
/// Procedure id shared by every packet in the session family.
pub procedure_id: u32,
/// Hook id for the live session instance.
pub hook_id: HookID,
}
/// Stable identity for one generated procedure view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcedureKey {
/// Leaf id that owns the generated procedure family.
pub leaf_id: u32,
/// Procedure id handled by this one-shot procedure family.
pub procedure_id: u32,
}
+12 -404
View File
@@ -1,406 +1,14 @@
use alloc::{collections::BTreeMap, vec::Vec};
//! Caller-owned interface state for UI frontends.
//!
//! Protocol leaves stay headless. When a UI wants packet flow, timing, or render
//! state, it passes an [`InterfaceStore`] through the feature-gated interface path.
use crate::protocol::{EndpointError, HookID, Packet, SessionStatus};
mod event;
mod key;
mod store;
mod view;
/// Caller-owned view and packet-flow store for interface frontends.
///
/// Generated leaves receive a mutable reference to this store during interface-aware
/// updates. They decide which leaf/session/procedure keys to touch, but the storage
/// itself stays with the renderer or application shell so protocol state remains
/// headless and reusable.
pub struct InterfaceStore {
next_sequence: u64,
now_ns: Option<u64>,
events: Vec<InterfaceEvent>,
sessions: BTreeMap<SessionKey, SessionView>,
procedures: BTreeMap<ProcedureKey, ProcedureView>,
}
impl InterfaceStore {
/// Creates an empty caller-owned interface store.
pub fn new() -> Self {
Self {
next_sequence: 0,
now_ns: None,
events: Vec::new(),
sessions: BTreeMap::new(),
procedures: BTreeMap::new(),
}
}
/// Sets the timestamp attached to later events.
///
/// The core crate stays `no_std`, so the caller supplies time from its runtime.
/// Passing `None` keeps event ordering without pretending the protocol owns a
/// clock.
pub fn set_now_ns(&mut self, now_ns: Option<u64>) {
self.now_ns = now_ns;
}
/// Returns the timestamp that will be attached to new events.
pub fn now_ns(&self) -> Option<u64> {
self.now_ns
}
/// Returns all recorded events in insertion order.
pub fn events(&self) -> &[InterfaceEvent] {
&self.events
}
/// Returns all session views keyed by leaf, procedure, and hook id.
pub fn session_views(&self) -> &BTreeMap<SessionKey, SessionView> {
&self.sessions
}
/// Returns all procedure views keyed by leaf and procedure id.
pub fn procedure_views(&self) -> &BTreeMap<ProcedureKey, ProcedureView> {
&self.procedures
}
/// Returns or creates the view for a hook-backed session.
pub fn session_view_mut(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) -> &mut SessionView {
self.sessions
.entry(SessionKey {
leaf_id,
procedure_id,
hook_id,
})
.or_insert_with(SessionView::new)
}
/// Returns or creates the view for a one-shot procedure family.
pub fn procedure_view_mut(&mut self, leaf_id: u32, procedure_id: u32) -> &mut ProcedureView {
self.procedures
.entry(ProcedureKey {
leaf_id,
procedure_id,
})
.or_insert_with(ProcedureView::new)
}
/// Records a packet delivered to a generated leaf.
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::Inbound {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records that a packet was queued for an existing session inbox.
pub fn record_session_packet_queued(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionPacketQueued {
procedure_id,
hook_id,
},
);
self.session_view_mut(leaf_id, procedure_id, hook_id)
.events
.push(index);
}
/// Records successful creation of a new session state.
pub fn record_session_created(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionCreated {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::Running;
view.events.push(index);
}
/// Records rejection of a packet that could not create a session.
pub fn record_session_rejected(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionRejected {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::Rejected;
view.events.push(index);
}
/// Records one session update tick.
pub fn record_session_update(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::SessionUpdated {
procedure_id,
hook_id,
status,
started_ns,
finished_ns: self.now_ns,
},
);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
view.status = SessionViewStatus::from_session_status(status);
view.events.push(index);
}
/// Records one procedure call.
pub fn record_procedure_call(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::ProcedureCalled {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
self.procedure_view_mut(leaf_id, procedure_id)
.events
.push(index);
}
/// Records a packet emitted by leaf logic before route retry handling.
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::OutboundQueued {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a route attempt for a queued outbound packet.
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteAttempt {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a successful route attempt.
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteSuccess {
packet: packet.clone(),
},
);
self.link_packet_event(leaf_id, packet, index);
}
/// Records a failed route attempt without removing the packet from retry state.
pub fn record_route_failure(&mut self, leaf_id: u32, packet: &Packet, error: EndpointError) {
let index = self.push_event(
leaf_id,
InterfaceEventKind::RouteFailure {
packet: packet.clone(),
error,
},
);
self.link_packet_event(leaf_id, packet, index);
}
fn push_event(&mut self, leaf_id: u32, kind: InterfaceEventKind) -> usize {
let sequence = self.next_sequence;
self.next_sequence = self.next_sequence.wrapping_add(1);
let index = self.events.len();
self.events.push(InterfaceEvent {
sequence,
time_ns: self.now_ns,
leaf_id,
kind,
});
index
}
fn link_packet_event(&mut self, leaf_id: u32, packet: &Packet, index: usize) {
self.session_view_mut(leaf_id, packet.procedure_id, packet.hook_id)
.events
.push(index);
}
}
impl Default for InterfaceStore {
fn default() -> Self {
Self::new()
}
}
/// Reborrows an optional interface store for one helper call.
///
/// Generated leaf templates pass the same optional store through several helper
/// calls in one update. This small function keeps that reborrow explicit and avoids
/// every generated call site having to spell out `Option<&mut &mut T>` plumbing.
pub fn borrow_store<'a>(
store: &'a mut Option<&mut InterfaceStore>,
) -> Option<&'a mut InterfaceStore> {
store.as_mut().map(|store| &mut **store)
}
/// Stable identity for one generated session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SessionKey {
pub leaf_id: u32,
pub procedure_id: u32,
pub hook_id: HookID,
}
/// Stable identity for one generated procedure view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcedureKey {
pub leaf_id: u32,
pub procedure_id: u32,
}
/// Ordered event stored by [`InterfaceStore`].
pub struct InterfaceEvent {
pub sequence: u64,
pub time_ns: Option<u64>,
pub leaf_id: u32,
pub kind: InterfaceEventKind,
}
/// Interface-visible event emitted by generated helpers.
pub enum InterfaceEventKind {
Inbound {
packet: Packet,
},
SessionPacketQueued {
procedure_id: u32,
hook_id: HookID,
},
SessionCreated {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
SessionRejected {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
SessionUpdated {
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
ProcedureCalled {
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
finished_ns: Option<u64>,
},
OutboundQueued {
packet: Packet,
},
RouteAttempt {
packet: Packet,
},
RouteSuccess {
packet: Packet,
},
RouteFailure {
packet: Packet,
error: EndpointError,
},
}
/// Caller-owned render view for one hook-backed session.
pub struct SessionView {
pub status: SessionViewStatus,
pub events: Vec<usize>,
}
impl SessionView {
fn new() -> Self {
Self {
status: SessionViewStatus::Pending,
events: Vec::new(),
}
}
}
/// Caller-owned render view for one one-shot procedure family.
pub struct ProcedureView {
pub events: Vec<usize>,
}
impl ProcedureView {
fn new() -> Self {
Self { events: Vec::new() }
}
}
/// Interface lifecycle state for one session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionViewStatus {
Pending,
Running,
Closing,
Closed,
Rejected,
}
impl SessionViewStatus {
fn from_session_status(status: SessionStatus) -> Self {
match status {
SessionStatus::Running => Self::Running,
SessionStatus::Closing => Self::Closing,
SessionStatus::Closed => Self::Closed,
}
}
}
pub use event::{InterfaceEvent, InterfaceEventKind};
pub use key::{ProcedureKey, SessionKey};
pub use store::InterfaceStore;
pub use view::{ProcedureView, SessionView, SessionViewStatus};
+311
View File
@@ -0,0 +1,311 @@
use alloc::{collections::BTreeMap, vec::Vec};
use crate::{
interface::{
InterfaceEvent, InterfaceEventKind, ProcedureKey, ProcedureView, SessionKey, SessionView,
SessionViewStatus,
},
protocol::{EndpointError, HookID, Packet, SessionStatus},
};
/// Caller-owned view and packet-flow store for interface frontends.
///
/// Generated leaves receive a mutable reference to this store during interface-aware
/// updates. They decide which leaf/session/procedure keys to touch, but the storage
/// itself stays with the renderer or application shell so protocol state remains
/// headless and reusable.
pub struct InterfaceStore {
next_sequence: u64,
now_ns: Option<u64>,
events: Vec<InterfaceEvent>,
sessions: BTreeMap<SessionKey, SessionView>,
procedures: BTreeMap<ProcedureKey, ProcedureView>,
}
impl InterfaceStore {
/// Creates an empty caller-owned interface store.
pub fn new() -> Self {
Self {
next_sequence: 0,
now_ns: None,
events: Vec::new(),
sessions: BTreeMap::new(),
procedures: BTreeMap::new(),
}
}
/// Sets the timestamp attached to later events.
///
/// The core crate stays `no_std`, so the caller supplies time from its runtime.
/// Passing `None` keeps event ordering without pretending the protocol owns a
/// clock.
pub fn set_now_ns(&mut self, now_ns: Option<u64>) {
self.now_ns = now_ns;
}
/// Returns the timestamp that will be attached to new events.
pub fn now_ns(&self) -> Option<u64> {
self.now_ns
}
/// Returns all recorded events in insertion order.
pub fn events(&self) -> &[InterfaceEvent] {
&self.events
}
/// Returns all session views keyed by leaf, procedure, and hook id.
pub fn session_views(&self) -> &BTreeMap<SessionKey, SessionView> {
&self.sessions
}
/// Returns all procedure views keyed by leaf and procedure id.
pub fn procedure_views(&self) -> &BTreeMap<ProcedureKey, ProcedureView> {
&self.procedures
}
/// Returns or creates the view for a hook-backed session.
pub fn session_view_mut(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) -> &mut SessionView {
self.sessions
.entry(SessionKey {
leaf_id,
procedure_id,
hook_id,
})
.or_insert_with(SessionView::new)
}
/// Returns or creates the view for a one-shot procedure family.
pub fn procedure_view_mut(&mut self, leaf_id: u32, procedure_id: u32) -> &mut ProcedureView {
self.procedures
.entry(ProcedureKey {
leaf_id,
procedure_id,
})
.or_insert_with(ProcedureView::new)
}
/// Records a packet delivered to a generated leaf.
pub fn record_inbound(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::Inbound {
packet: packet.clone(),
},
);
}
/// Records that a packet was queued for an existing session inbox.
pub fn record_session_packet_queued(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
None,
InterfaceEventKind::SessionPacketQueued {
procedure_id,
hook_id,
},
);
}
/// Records successful creation of a new session state.
pub fn record_session_created(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::Running),
InterfaceEventKind::SessionCreated {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records rejection of a packet that could not create a session.
pub fn record_session_rejected(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::Rejected),
InterfaceEventKind::SessionRejected {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records one session update tick.
pub fn record_session_update(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: SessionStatus,
started_ns: Option<u64>,
) {
self.push_session_event(
leaf_id,
procedure_id,
hook_id,
Some(SessionViewStatus::from_session_status(status)),
InterfaceEventKind::SessionUpdated {
procedure_id,
hook_id,
status,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records one procedure call.
pub fn record_procedure_call(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
started_ns: Option<u64>,
) {
self.push_procedure_event(
leaf_id,
procedure_id,
InterfaceEventKind::ProcedureCalled {
procedure_id,
hook_id,
started_ns,
finished_ns: self.now_ns,
},
);
}
/// Records a packet emitted by leaf logic before route retry handling.
pub fn record_outbound_queued(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::OutboundQueued {
packet: packet.clone(),
},
);
}
/// Records a route attempt for a queued outbound packet.
pub fn record_route_attempt(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteAttempt {
packet: packet.clone(),
},
);
}
/// Records a successful route attempt.
pub fn record_route_success(&mut self, leaf_id: u32, packet: &Packet) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteSuccess {
packet: packet.clone(),
},
);
}
/// Records a failed route attempt without removing the packet from retry state.
pub fn record_route_failure(&mut self, leaf_id: u32, packet: &Packet, error: EndpointError) {
self.push_packet_event(
leaf_id,
packet,
InterfaceEventKind::RouteFailure {
packet: packet.clone(),
error,
},
);
}
fn push_packet_event(&mut self, leaf_id: u32, packet: &Packet, kind: InterfaceEventKind) {
let index = self.push_event(leaf_id, kind);
self.link_packet_event(leaf_id, packet, index);
}
fn push_session_event(
&mut self,
leaf_id: u32,
procedure_id: u32,
hook_id: HookID,
status: Option<SessionViewStatus>,
kind: InterfaceEventKind,
) {
let index = self.push_event(leaf_id, kind);
let view = self.session_view_mut(leaf_id, procedure_id, hook_id);
if let Some(status) = status {
view.status = status;
}
view.events.push(index);
}
fn push_procedure_event(&mut self, leaf_id: u32, procedure_id: u32, kind: InterfaceEventKind) {
let index = self.push_event(leaf_id, kind);
self.procedure_view_mut(leaf_id, procedure_id)
.events
.push(index);
}
fn push_event(&mut self, leaf_id: u32, kind: InterfaceEventKind) -> usize {
let sequence = self.next_sequence;
self.next_sequence = self.next_sequence.wrapping_add(1);
let index = self.events.len();
self.events.push(InterfaceEvent {
sequence,
time_ns: self.now_ns,
leaf_id,
kind,
});
index
}
fn link_packet_event(&mut self, leaf_id: u32, packet: &Packet, index: usize) {
self.session_view_mut(leaf_id, packet.procedure_id, packet.hook_id)
.events
.push(index);
}
}
impl Default for InterfaceStore {
fn default() -> Self {
Self::new()
}
}
+65
View File
@@ -0,0 +1,65 @@
use alloc::vec::Vec;
use crate::protocol::SessionStatus;
/// Caller-owned render view for one hook-backed session.
pub struct SessionView {
/// Latest known lifecycle status.
pub status: SessionViewStatus,
/// Indices into the store's append-only event list.
pub events: Vec<usize>,
}
impl SessionView {
/// Creates an empty pending view.
pub(crate) fn new() -> Self {
Self {
status: SessionViewStatus::Pending,
events: Vec::new(),
}
}
}
/// Caller-owned render view for one one-shot procedure family.
pub struct ProcedureView {
/// Indices into the store's append-only event list.
pub events: Vec<usize>,
}
impl ProcedureView {
/// Creates an empty procedure view.
pub(crate) fn new() -> Self {
Self { events: Vec::new() }
}
}
/// Interface lifecycle state for one session view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionViewStatus {
/// The view exists because packets referenced it, but no live state exists yet.
Pending,
/// The session is active.
Running,
/// The session is winding down but may still emit packets.
Closing,
/// The session reported that application work is complete.
Closed,
/// The leaf rejected the packet that would have created this session.
Rejected,
}
impl SessionViewStatus {
/// Converts the protocol session status into a renderable status.
pub(crate) fn from_session_status(status: SessionStatus) -> Self {
match status {
SessionStatus::Running => Self::Running,
SessionStatus::Closing => Self::Closing,
SessionStatus::Closed => Self::Closed,
}
}
}