Split protocol internals by responsibility

This commit is contained in:
Michael Mikovsky
2026-06-01 13:39:48 -06:00
parent b4344a8d6a
commit 921ea838c4
14 changed files with 533 additions and 491 deletions
+39
View File
@@ -0,0 +1,39 @@
use crate::protocol::{Endpoint, EndpointName};
impl Endpoint {
/// Registers an adjacent endpoint and returns whether this is a new edge.
///
/// Endpoint routing tables are intentionally tiny in the minimized firmware
/// profile. A linear vector keeps that profile from linking tree-map machinery
/// while preserving the old set semantics: duplicate connection registrations do
/// not create duplicate route entries.
pub fn add_connection(&mut self, remote_id: EndpointName, is_authority: bool) -> bool {
let connection = (remote_id, is_authority);
if self.connection_contains(remote_id, is_authority) {
false
} else {
self.connections.push(connection);
true
}
}
/// Removes an adjacent endpoint registration and reports whether it existed.
pub fn remove_connection(&mut self, remote_id: EndpointName, is_authority: bool) -> bool {
let Some(index) = self
.connections
.iter()
.position(|connection| *connection == (remote_id, is_authority))
else {
return false;
};
self.connections.remove(index);
true
}
/// Returns whether an adjacent endpoint is registered in the requested direction.
pub fn connection_contains(&self, remote_id: EndpointName, is_authority: bool) -> bool {
self.connections.contains(&(remote_id, is_authority))
}
}
+77
View File
@@ -0,0 +1,77 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, EndpointError, Packet};
use super::HookID;
impl Endpoint {
/// Returns the destination path for packets sent back over `hook_id`.
///
/// Hooks record the adjacent peer that paved the return channel. This helper turns
/// that peer into the packet path required by the current router: parent peers map
/// to the parent path, and child peers map to the direct child path. Session logic
/// should not store this path itself.
pub(crate) fn hook_path(&self, hook_id: HookID) -> Result<Vec<u32>, EndpointError> {
let peer = self
.hook_peer(hook_id)
.ok_or(EndpointError::UnknownHook { hook_id })?;
if self.path.is_empty() {
return Err(EndpointError::EndpointPathUnset);
}
if self.path.len() > 1 && self.path[self.path.len() - 2] == peer {
Ok(self.path[..self.path.len() - 1].to_vec())
} else {
let mut path = self.path.clone();
path.push(peer);
Ok(path)
}
}
/// Routes raw response data over an existing hook immediately.
///
/// This is the compact session-output path: it avoids an intermediate context and
/// retry queue. If a final packet cannot route, the local hook is still removed so
/// an implant does not retain dead hook state forever.
pub fn send_hook_raw(
&mut self,
hook_id: HookID,
procedure_id: u32,
data: Vec<u8>,
end_hook: bool,
) -> Result<(), EndpointError> {
let path = self.hook_path(hook_id)?;
let packet = Packet {
hook_id,
end_hook,
path,
procedure_id,
data,
};
let result = self.add_outbound(packet);
if result.is_err() && end_hook {
self.close_hook(hook_id);
}
result
}
/// Routes a one-byte-opcode response frame over an existing hook immediately.
pub fn send_hook_frame(
&mut self,
hook_id: HookID,
procedure_id: u32,
opcode: u8,
payload: &[u8],
end_hook: bool,
) -> Result<(), EndpointError> {
let mut data = Vec::with_capacity(payload.len() + 1);
data.push(opcode);
data.extend_from_slice(payload);
self.send_hook_raw(hook_id, procedure_id, data, end_hook)
}
}
+31 -73
View File
@@ -1,6 +1,4 @@
use alloc::vec::Vec; use crate::protocol::{Endpoint, EndpointError, EndpointName};
use crate::protocol::{Endpoint, EndpointError, EndpointName, Packet};
/// Compact identifier for one routed return channel. /// Compact identifier for one routed return channel.
/// ///
@@ -86,76 +84,6 @@ impl Endpoint {
self.close_hook(hook_id) self.close_hook(hook_id)
} }
/// Returns the destination path for packets sent back over `hook_id`.
///
/// Hooks record the adjacent peer that paved the return channel. This helper turns
/// that peer into the packet path required by the current router: parent peers map
/// to the parent path, and child peers map to the direct child path. Session logic
/// should not store this path itself.
pub(crate) fn hook_path(&self, hook_id: HookID) -> Result<Vec<u32>, EndpointError> {
let peer = self
.hook_peer(hook_id)
.ok_or(EndpointError::UnknownHook { hook_id })?;
if self.path.is_empty() {
return Err(EndpointError::EndpointPathUnset);
}
if self.path.len() > 1 && self.path[self.path.len() - 2] == peer {
Ok(self.path[..self.path.len() - 1].to_vec())
} else {
let mut path = self.path.clone();
path.push(peer);
Ok(path)
}
}
/// Routes raw response data over an existing hook immediately.
///
/// This is the compact session-output path: it avoids an intermediate context and
/// retry queue. If a final packet cannot route, the local hook is still removed so
/// an implant does not retain dead hook state forever.
pub fn send_hook_raw(
&mut self,
hook_id: HookID,
procedure_id: u32,
data: Vec<u8>,
end_hook: bool,
) -> Result<(), EndpointError> {
let path = self.hook_path(hook_id)?;
let packet = Packet {
hook_id,
end_hook,
path,
procedure_id,
data,
};
let result = self.add_outbound(packet);
if result.is_err() && end_hook {
self.close_hook(hook_id);
}
result
}
/// Routes a one-byte-opcode response frame over an existing hook immediately.
pub fn send_hook_frame(
&mut self,
hook_id: HookID,
procedure_id: u32,
opcode: u8,
payload: &[u8],
end_hook: bool,
) -> Result<(), EndpointError> {
let mut data = Vec::with_capacity(payload.len() + 1);
data.push(opcode);
data.extend_from_slice(payload);
self.send_hook_raw(hook_id, procedure_id, data, end_hook)
}
/// Validates that `actual_peer` is the peer allowed to use `hook_id`. /// Validates that `actual_peer` is the peer allowed to use `hook_id`.
pub(crate) fn ensure_hook_peer( pub(crate) fn ensure_hook_peer(
&self, &self,
@@ -186,4 +114,34 @@ impl Endpoint {
pub(crate) fn close_hook(&mut self, hook_id: HookID) -> bool { pub(crate) fn close_hook(&mut self, hook_id: HookID) -> bool {
self.hook_remove(hook_id).is_some() self.hook_remove(hook_id).is_some()
} }
/// Inserts or updates a hook and returns the previously associated peer.
pub(crate) fn hook_insert(
&mut self,
hook_id: HookID,
peer: EndpointName,
) -> Option<EndpointName> {
if let Some((_, existing_peer)) = self
.hooks
.iter_mut()
.find(|(existing_hook, _)| *existing_hook == hook_id)
{
let previous = *existing_peer;
*existing_peer = peer;
Some(previous)
} else {
self.hooks.push((hook_id, peer));
None
}
}
/// Removes a hook and returns the peer it pointed at.
pub(crate) fn hook_remove(&mut self, hook_id: HookID) -> Option<EndpointName> {
let index = self
.hooks
.iter()
.position(|(existing_hook, _)| *existing_hook == hook_id)?;
Some(self.hooks.remove(index).1)
}
} }
+17 -196
View File
@@ -1,4 +1,7 @@
mod connections;
mod hook_output;
mod hooks; mod hooks;
mod queues;
mod routing; mod routing;
pub use hooks::HookID; pub use hooks::HookID;
@@ -7,28 +10,34 @@ use alloc::vec::Vec;
use crate::{ use crate::{
crypto::Counter, crypto::Counter,
protocol::{ConnectionSet, EndpointName, HookMap, Packet, PacketQueue, Path, RouteMap}, protocol::{ConnectionSet, HookMap, Path, RouteMap},
}; };
/// Local routing state for one protocol node.
///
/// `Endpoint` deliberately owns only route, hook, and connection tables. Leaves are
/// caller-owned concrete values, which keeps small firmware-style binaries from
/// linking dynamic leaf registries or boxed trait objects.
pub struct Endpoint { pub struct Endpoint {
// This endpoint's identifier /// This endpoint's identifier.
pub id: u32, pub id: u32,
// A counter that creates unique hook IDs. /// Counter used to allocate locally unique hook ids.
pub(crate) last_hook: Counter, pub(crate) last_hook: Counter,
// Absolute path for this node. Must be set by some leaf /// Absolute path for this node. An empty path means routing is not initialized.
pub path: Path, pub path: Path,
// Map of connections so that we can know what is connected /// Adjacent endpoints and whether each adjacent endpoint is upstream/authority.
// and which endpoints are authorities
pub connections: ConnectionSet, pub connections: ConnectionSet,
// Local list of hooks. /// Active hook id to adjacent peer mappings.
pub(crate) hooks: HookMap, pub(crate) hooks: HookMap,
// Map of endpoints to packet queues /// Packets delivered locally and waiting for leaf consumption.
pub(crate) inbound: RouteMap, pub(crate) inbound: RouteMap,
/// Packets queued for adjacent endpoints and waiting for transport leaves.
pub(crate) outbound: RouteMap, pub(crate) outbound: RouteMap,
} }
@@ -53,192 +62,4 @@ impl Endpoint {
outbound: Vec::new(), outbound: Vec::new(),
} }
} }
/// Registers an adjacent endpoint and returns whether this is a new edge.
///
/// Endpoint routing tables are intentionally tiny in the minimized firmware
/// profile. A linear vector keeps that profile from linking tree-map machinery
/// while preserving the old set semantics: duplicate connection registrations do
/// not create duplicate route entries.
pub fn add_connection(&mut self, remote_id: EndpointName, is_authority: bool) -> bool {
let connection = (remote_id, is_authority);
if self.connection_contains(remote_id, is_authority) {
false
} else {
self.connections.push(connection);
true
}
}
/// Removes an adjacent endpoint registration and reports whether it existed.
pub fn remove_connection(&mut self, remote_id: EndpointName, is_authority: bool) -> bool {
let Some(index) = self
.connections
.iter()
.position(|connection| *connection == (remote_id, is_authority))
else {
return false;
};
self.connections.remove(index);
true
}
/// Returns whether an adjacent endpoint is registered in the requested direction.
pub fn connection_contains(&self, remote_id: EndpointName, is_authority: bool) -> bool {
self.connections.contains(&(remote_id, is_authority))
}
/// Run a function over all inbound packets with some ID then clear it.
pub fn take_inbound_clear<F>(&mut self, path: u32, f: F)
where
F: FnMut(&Packet),
{
Self::take_clear(path, f, &mut self.inbound);
}
/// Drain inbound packets for `path` that match `predicate` and preserve the rest.
///
/// Generated leaf dispatch uses this instead of [`Self::take_inbound_clear`] so
/// one leaf can consume only its procedure or session packets without stealing
/// traffic intended for another leaf. Matching packets are passed by value because
/// most handlers need to move payload bytes into application state; unmatched
/// packets are reinserted in their original FIFO order.
pub fn take_inbound_matching<P, F>(&mut self, path: u32, mut predicate: P, mut f: F)
where
P: FnMut(&Packet) -> bool,
F: FnMut(Packet),
{
let Some(mut queue) = Self::route_remove(path, &mut self.inbound) else {
return;
};
let mut unmatched = Vec::new();
while let Some(packet) = queue.pop_front() {
if predicate(&packet) {
f(packet);
} else {
unmatched.push(packet);
}
}
if !unmatched.is_empty() {
Self::route_queue_mut(path, &mut self.inbound).extend(unmatched);
}
}
/// Run a function over all outbound packets with some ID then clear it.
pub fn take_outbound_clear<F>(&mut self, path: u32, f: F)
where
F: FnMut(&Packet),
{
Self::take_clear(path, f, &mut self.outbound);
}
fn take_clear<F>(path: u32, mut f: F, queue: &mut RouteMap)
where
F: FnMut(&Packet),
{
if let Some(queue) = Self::route_queue_mut_existing(path, queue) {
for packet in queue.iter() {
f(packet);
}
queue.clear();
}
}
/// Appends a packet to the route queue for `endpoint`.
pub(crate) fn route_push(endpoint: EndpointName, packet: Packet, routes: &mut RouteMap) {
Self::route_queue_mut(endpoint, routes).push_back(packet);
}
/// Returns the route queue for `endpoint` if one exists.
#[cfg(test)]
pub(crate) fn route_get(endpoint: EndpointName, routes: &RouteMap) -> Option<&PacketQueue> {
routes
.iter()
.find(|(queued_endpoint, _)| *queued_endpoint == endpoint)
.map(|(_, queue)| queue)
}
/// Removes and returns the queue for `endpoint`.
pub(crate) fn route_remove(
endpoint: EndpointName,
routes: &mut RouteMap,
) -> Option<PacketQueue> {
let index = routes
.iter()
.position(|(queued_endpoint, _)| *queued_endpoint == endpoint)?;
Some(routes.remove(index).1)
}
/// Returns whether a route queue exists for `endpoint`.
#[cfg(test)]
pub(crate) fn route_contains(endpoint: EndpointName, routes: &RouteMap) -> bool {
Self::route_get(endpoint, routes).is_some()
}
/// Returns whether no route queues are present.
#[cfg(test)]
pub(crate) fn routes_is_empty(routes: &RouteMap) -> bool {
routes.is_empty()
}
/// Returns the route queue for `endpoint`, creating it on first use.
fn route_queue_mut(endpoint: EndpointName, routes: &mut RouteMap) -> &mut PacketQueue {
if let Some(index) = routes
.iter()
.position(|(queued_endpoint, _)| *queued_endpoint == endpoint)
{
&mut routes[index].1
} else {
routes.push((endpoint, PacketQueue::new()));
&mut routes.last_mut().unwrap().1
}
}
/// Returns the existing route queue for `endpoint` without allocating a new one.
fn route_queue_mut_existing(
endpoint: EndpointName,
routes: &mut RouteMap,
) -> Option<&mut PacketQueue> {
routes
.iter_mut()
.find(|(queued_endpoint, _)| *queued_endpoint == endpoint)
.map(|(_, queue)| queue)
}
/// Inserts or updates a hook and returns the previously associated peer.
pub(crate) fn hook_insert(
&mut self,
hook_id: HookID,
peer: EndpointName,
) -> Option<EndpointName> {
if let Some((_, existing_peer)) = self
.hooks
.iter_mut()
.find(|(existing_hook, _)| *existing_hook == hook_id)
{
let previous = *existing_peer;
*existing_peer = peer;
Some(previous)
} else {
self.hooks.push((hook_id, peer));
None
}
}
/// Removes a hook and returns the peer it pointed at.
pub(crate) fn hook_remove(&mut self, hook_id: HookID) -> Option<EndpointName> {
let index = self
.hooks
.iter()
.position(|(existing_hook, _)| *existing_hook == hook_id)?;
Some(self.hooks.remove(index).1)
}
} }
+127
View File
@@ -0,0 +1,127 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, EndpointName, Packet, PacketQueue, RouteMap};
impl Endpoint {
/// Runs a function over all inbound packets for `path`, then clears that queue.
pub fn take_inbound_clear<F>(&mut self, path: u32, f: F)
where
F: FnMut(&Packet),
{
Self::take_clear(path, f, &mut self.inbound);
}
/// Drain inbound packets for `path` that match `predicate` and preserve the rest.
///
/// Generated leaf dispatch uses this instead of [`Self::take_inbound_clear`] so
/// one leaf can consume only its procedure or session packets without stealing
/// traffic intended for another leaf. Matching packets are passed by value because
/// most handlers need to move payload bytes into application state; unmatched
/// packets are reinserted in their original FIFO order.
pub fn take_inbound_matching<P, F>(&mut self, path: u32, mut predicate: P, mut f: F)
where
P: FnMut(&Packet) -> bool,
F: FnMut(Packet),
{
let Some(mut queue) = Self::route_remove(path, &mut self.inbound) else {
return;
};
let mut unmatched = Vec::new();
while let Some(packet) = queue.pop_front() {
if predicate(&packet) {
f(packet);
} else {
unmatched.push(packet);
}
}
if !unmatched.is_empty() {
Self::route_queue_mut(path, &mut self.inbound).extend(unmatched);
}
}
/// Runs a function over all outbound packets for `path`, then clears that queue.
pub fn take_outbound_clear<F>(&mut self, path: u32, f: F)
where
F: FnMut(&Packet),
{
Self::take_clear(path, f, &mut self.outbound);
}
fn take_clear<F>(path: u32, mut f: F, queue: &mut RouteMap)
where
F: FnMut(&Packet),
{
if let Some(queue) = Self::route_queue_mut_existing(path, queue) {
for packet in queue.iter() {
f(packet);
}
queue.clear();
}
}
/// Appends a packet to the route queue for `endpoint`.
pub(crate) fn route_push(endpoint: EndpointName, packet: Packet, routes: &mut RouteMap) {
Self::route_queue_mut(endpoint, routes).push_back(packet);
}
/// Returns the route queue for `endpoint` if one exists.
#[cfg(test)]
pub(crate) fn route_get(endpoint: EndpointName, routes: &RouteMap) -> Option<&PacketQueue> {
routes
.iter()
.find(|(queued_endpoint, _)| *queued_endpoint == endpoint)
.map(|(_, queue)| queue)
}
/// Removes and returns the queue for `endpoint`.
pub(crate) fn route_remove(
endpoint: EndpointName,
routes: &mut RouteMap,
) -> Option<PacketQueue> {
let index = routes
.iter()
.position(|(queued_endpoint, _)| *queued_endpoint == endpoint)?;
Some(routes.remove(index).1)
}
/// Returns whether a route queue exists for `endpoint`.
#[cfg(test)]
pub(crate) fn route_contains(endpoint: EndpointName, routes: &RouteMap) -> bool {
Self::route_get(endpoint, routes).is_some()
}
/// Returns whether no route queues are present.
#[cfg(test)]
pub(crate) fn routes_is_empty(routes: &RouteMap) -> bool {
routes.is_empty()
}
/// Returns the route queue for `endpoint`, creating it on first use.
fn route_queue_mut(endpoint: EndpointName, routes: &mut RouteMap) -> &mut PacketQueue {
if let Some(index) = routes
.iter()
.position(|(queued_endpoint, _)| *queued_endpoint == endpoint)
{
&mut routes[index].1
} else {
routes.push((endpoint, PacketQueue::new()));
&mut routes.last_mut().unwrap().1
}
}
/// Returns the existing route queue for `endpoint` without allocating a new one.
fn route_queue_mut_existing(
endpoint: EndpointName,
routes: &mut RouteMap,
) -> Option<&mut PacketQueue> {
routes
.iter_mut()
.find(|(queued_endpoint, _)| *queued_endpoint == endpoint)
.map(|(_, queue)| queue)
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::protocol::{Endpoint, Packet, ProcedureOut};
#[cfg(feature = "interface_ratatui")]
use crate::interface::ProcedureView;
/// Contract implemented by one generated one-packet procedure handler.
///
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
/// or health checks. Long-running conversations should use [`Session`](crate::protocol::Session)
/// so final packet cleanup and retries remain tied to hook state.
pub trait Procedure<L> {
/// Outer packet procedure id handled by this procedure.
const PROCEDURE_ID: u32;
/// Handles one packet and optionally queues response packets in `out`.
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
#[cfg(feature = "interface_ratatui")]
fn render_ratatui(
_: &L,
_: &mut ProcedureView,
_: &mut ratatui::Frame<'_>,
_: ratatui::layout::Rect,
) {
}
}
+7
View File
@@ -0,0 +1,7 @@
//! One-shot procedure contracts and response output helpers.
mod contract;
mod out;
pub use contract::Procedure;
pub use out::ProcedureOut;
@@ -1,33 +1,8 @@
use alloc::vec::Vec; use alloc::vec::Vec;
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue}; use crate::protocol::{HookID, Packet, PacketQueue};
#[cfg(feature = "interface_ratatui")] /// Output accumulator passed to [`Procedure::handle`](super::Procedure::handle).
use crate::interface::ProcedureView;
/// Contract implemented by one generated one-packet procedure handler.
///
/// Procedures are for stateless or short-lived operations such as ping, capabilities,
/// or health checks. Long-running conversations should use [`Session`] so final
/// packet cleanup and retries remain tied to hook state.
pub trait Procedure<L> {
/// Outer packet procedure id handled by this procedure.
const PROCEDURE_ID: u32;
/// Handles one packet and optionally queues response packets in `out`.
fn handle(leaf: &mut L, endpoint: &mut Endpoint, packet: Packet, out: &mut ProcedureOut);
#[cfg(feature = "interface_ratatui")]
fn render_ratatui(
_: &L,
_: &mut ProcedureView,
_: &mut ratatui::Frame<'_>,
_: ratatui::layout::Rect,
) {
}
}
/// Output accumulator passed to [`Procedure::handle`].
pub struct ProcedureOut { pub struct ProcedureOut {
hook_id: HookID, hook_id: HookID,
reply_path: Vec<u32>, reply_path: Vec<u32>,
-195
View File
@@ -1,195 +0,0 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, HookID, Packet, PacketQueue};
#[cfg(feature = "interface_ratatui")]
use crate::interface::SessionView;
/// Contract implemented by one hook-backed generated session family.
///
/// A session family maps one outer `procedure_id` to many live hook instances. The
/// generated leaf owns packet grouping, retry-safe output flushing, and final cleanup;
/// the session value owns one hook's application behavior and mutable state.
///
/// # Example
///
/// ```rust,ignore
/// impl Session<MyLeafState> for MySessionState {
/// const PROCEDURE_ID: u32 = 7;
///
/// fn init(
/// leaf: &mut MyLeafState,
/// packet: Packet,
/// ) -> Result<Self, SessionInitError> {
/// Ok(MySessionState::from_open(leaf, packet))
/// }
///
/// fn update(
/// leaf: &mut MyLeafState,
/// session: &mut Self,
/// incoming: &mut PacketQueue,
/// endpoint: &mut Endpoint,
/// ) -> SessionStatus {
/// while let Some(packet) = incoming.pop_front() {
/// session.apply(leaf, packet, endpoint);
/// }
/// SessionStatus::Running
/// }
/// }
/// ```
pub trait Session<L>: Sized {
/// Outer packet procedure id used by every packet in this session family.
const PROCEDURE_ID: u32;
/// Creates one session value from a packet whose hook has no active session.
///
/// The generated runtime derives all response routing from hook state. Session
/// initialization therefore returns only application state or a protocol-level
/// rejection; it never stores or receives a caller reply path.
fn init(leaf: &mut L, packet: Packet) -> Result<Self, SessionInitError>;
/// Advances one active hook session.
///
/// The generated leaf calls this for every live session on each update tick so
/// sessions can poll external workers even when no new packet arrived. Session
/// output is routed immediately through `endpoint`; callers that need retry
/// semantics should keep their own compact application state and retry on a later
/// tick.
fn update(
leaf: &mut L,
session: &mut Self,
incoming: &mut PacketQueue,
endpoint: &mut Endpoint,
) -> SessionStatus;
#[cfg(feature = "interface_ratatui")]
fn render_ratatui(
_: &L,
_: &Self,
_: &mut SessionView,
_: &mut ratatui::Frame<'_>,
_: ratatui::layout::Rect,
) {
}
}
/// Error returned when a packet cannot create a new session.
pub enum SessionInitError {
/// The packet was intentionally consumed without creating state or sending output.
Rejected,
/// The packet was rejected with response data that should be sent on the same hook.
Response {
/// Raw `Packet::data` for the response frame.
data: Vec<u8>,
/// Whether the response should close the hook after successful routing.
end_hook: bool,
},
}
impl SessionInitError {
/// Creates a silent session rejection.
pub fn rejected() -> Self {
Self::Rejected
}
/// Creates a non-final response for a rejected session open.
pub fn response(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: false,
}
}
/// Creates a final response for a rejected session open.
pub fn response_final(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: true,
}
}
}
/// Session lifecycle status returned from [`Session::update`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStatus {
/// The session is active and should receive future update ticks.
Running,
/// The session is winding down but still needs future update ticks.
Closing,
/// The session has finished application work.
///
/// The generated leaf removes the entry after the update tick. Final packets are
/// routed immediately by the session before returning this status.
Closed,
}
/// Storage entry used by macro-generated session stores.
///
/// The fields are public so generated code in downstream crates can keep the update
/// loop straightforward and static. Handwritten leaves may also use this type, but it
/// is intentionally small rather than a full session framework.
pub struct SessionEntry<S> {
/// Hook id associated with this live session.
pub hook_id: HookID,
/// Application-owned session state.
pub state: S,
/// Packets delivered for this hook but not yet consumed by the session.
pub inbox: PacketQueue,
/// Whether application logic has finished and should be removed after update.
pub closed: bool,
}
/// Generated storage for one session family.
///
/// The macro only names this field and picks the concrete `Session` type. All update,
/// retry, and cleanup behavior lives in normal Rust helpers so the template stays
/// small and readable.
pub struct SessionFamily<S> {
/// Active hook-backed sessions for this family.
pub entries: Vec<SessionEntry<S>>,
}
impl<S> SessionFamily<S> {
/// Creates an empty session family.
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Counts packets retained by this family for retry or future session work.
pub fn pending_packet_count(&self) -> usize {
let mut count = 0usize;
for entry in &self.entries {
count += entry.inbox.len();
}
count
}
}
impl<S> Default for SessionFamily<S> {
fn default() -> Self {
Self::new()
}
}
impl<S> SessionEntry<S> {
/// Creates one active session entry for `hook_id`.
pub fn new(hook_id: HookID, state: S) -> Self {
Self {
hook_id,
state,
inbox: PacketQueue::new(),
closed: false,
}
}
}
+72
View File
@@ -0,0 +1,72 @@
use crate::protocol::{Endpoint, Packet, PacketQueue, SessionInitError, SessionStatus};
#[cfg(feature = "interface_ratatui")]
use crate::interface::SessionView;
/// Contract implemented by one hook-backed generated session family.
///
/// A session family maps one outer `procedure_id` to many live hook instances. The
/// generated leaf owns packet grouping, retry-safe output flushing, and final cleanup;
/// the session value owns one hook's application behavior and mutable state.
///
/// # Example
///
/// ```rust,ignore
/// impl Session<MyLeafState> for MySessionState {
/// const PROCEDURE_ID: u32 = 7;
///
/// fn init(
/// leaf: &mut MyLeafState,
/// packet: Packet,
/// ) -> Result<Self, SessionInitError> {
/// Ok(MySessionState::from_open(leaf, packet))
/// }
///
/// fn update(
/// leaf: &mut MyLeafState,
/// session: &mut Self,
/// incoming: &mut PacketQueue,
/// endpoint: &mut Endpoint,
/// ) -> SessionStatus {
/// while let Some(packet) = incoming.pop_front() {
/// session.apply(leaf, packet, endpoint);
/// }
/// SessionStatus::Running
/// }
/// }
/// ```
pub trait Session<L>: Sized {
/// Outer packet procedure id used by every packet in this session family.
const PROCEDURE_ID: u32;
/// Creates one session value from a packet whose hook has no active session.
///
/// The generated runtime derives all response routing from hook state. Session
/// initialization therefore returns only application state or a protocol-level
/// rejection; it never stores or receives a caller reply path.
fn init(leaf: &mut L, packet: Packet) -> Result<Self, SessionInitError>;
/// Advances one active hook session.
///
/// The generated leaf calls this for every live session on each update tick so
/// sessions can poll external workers even when no new packet arrived. Session
/// output is routed immediately through `endpoint`; callers that need retry
/// semantics should keep their own compact application state and retry on a later
/// tick.
fn update(
leaf: &mut L,
session: &mut Self,
incoming: &mut PacketQueue,
endpoint: &mut Endpoint,
) -> SessionStatus;
#[cfg(feature = "interface_ratatui")]
fn render_ratatui(
_: &L,
_: &Self,
_: &mut SessionView,
_: &mut ratatui::Frame<'_>,
_: ratatui::layout::Rect,
) {
}
}
+39
View File
@@ -0,0 +1,39 @@
use alloc::vec::Vec;
/// Error returned when a packet cannot create a new session.
pub enum SessionInitError {
/// The packet was intentionally consumed without creating state or sending output.
Rejected,
/// The packet was rejected with response data that should be sent on the same hook.
Response {
/// Raw `Packet::data` for the response frame.
data: Vec<u8>,
/// Whether the response should close the hook after successful routing.
end_hook: bool,
},
}
impl SessionInitError {
/// Creates a silent session rejection.
pub fn rejected() -> Self {
Self::Rejected
}
/// Creates a non-final response for a rejected session open.
pub fn response(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: false,
}
}
/// Creates a final response for a rejected session open.
pub fn response_final(data: Vec<u8>) -> Self {
Self::Response {
data,
end_hook: true,
}
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Hook-backed session contracts and generated session storage.
mod contract;
mod error;
mod status;
mod storage;
pub use contract::Session;
pub use error::SessionInitError;
pub use status::SessionStatus;
pub use storage::{SessionEntry, SessionFamily};
+15
View File
@@ -0,0 +1,15 @@
/// Session lifecycle status returned from [`Session::update`](super::Session::update).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStatus {
/// The session is active and should receive future update ticks.
Running,
/// The session is winding down but still needs future update ticks.
Closing,
/// The session has finished application work.
///
/// The generated leaf removes the entry after the update tick. Final packets are
/// routed immediately by the session before returning this status.
Closed,
}
+70
View File
@@ -0,0 +1,70 @@
use alloc::vec::Vec;
use crate::protocol::{HookID, PacketQueue};
/// Storage entry used by macro-generated session stores.
///
/// The fields are public so generated code in downstream crates can keep the update
/// loop straightforward and static. Handwritten leaves may also use this type, but it
/// is intentionally small rather than a full session framework.
pub struct SessionEntry<S> {
/// Hook id associated with this live session.
pub hook_id: HookID,
/// Application-owned session state.
pub state: S,
/// Packets delivered for this hook but not yet consumed by the session.
pub inbox: PacketQueue,
/// Whether application logic has finished and should be removed after update.
pub closed: bool,
}
/// Generated storage for one session family.
///
/// The macro only names this field and picks the concrete `Session` type. All update,
/// retry, and cleanup behavior lives in normal Rust helpers so the template stays
/// small and readable.
pub struct SessionFamily<S> {
/// Active hook-backed sessions for this family.
pub entries: Vec<SessionEntry<S>>,
}
impl<S> SessionFamily<S> {
/// Creates an empty session family.
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
/// Counts packets retained by this family for retry or future session work.
pub fn pending_packet_count(&self) -> usize {
let mut count = 0usize;
for entry in &self.entries {
count += entry.inbox.len();
}
count
}
}
impl<S> Default for SessionFamily<S> {
fn default() -> Self {
Self::new()
}
}
impl<S> SessionEntry<S> {
/// Creates one active session entry for `hook_id`.
pub fn new(hook_id: HookID, state: S) -> Self {
Self {
hook_id,
state,
inbox: PacketQueue::new(),
closed: false,
}
}
}