Skip to main content

prns_runtime/manifold/
interface_seam.rs

1use crate::interfaces::{
2    FrameSink, InterfaceDescriptor, InterfaceKind, InterfaceOriginKind, PacketPhyStats,
3};
4
5pub use prns_core::interfaces::{
6    frame_cap_for, BROADCAST_WIRE_FRAME_LEN, EMBEDDED_MAX_LINK_MTU, EMBEDDED_MAX_WIRE_FRAME_LEN,
7    MAX_WIRE_FRAME_LEN,
8};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum OutboundDropReason {
12    Disabled,
13    Disconnected,
14    TimedOut,
15    ContentionTimeout,
16    DutyLimited,
17    TransportFailure,
18    Rejected,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum OutboundDisposition {
23    Sent,
24    Dropped(OutboundDropReason),
25}
26
27/// One interface's side of the manifold boundary: inbound frames accumulate in [`inbound_sink`](Self::inbound_sink) and cross on [`commit_inbound`](Self::commit_inbound), and [`next_outbound`](Self::next_outbound) parks until the manifold has a frame for this interface to transmit. An outbound frame arrives already committed: the engine wrote it into the lane's slot and let go in its own synchronous step before `next_outbound` resolves, so the returned borrow points into the lane, never into the engine, and holding it across the transmit await pins nothing.
28#[allow(async_fn_in_trait)]
29pub trait InterfaceSeam {
30    fn interface_origin(&self) -> InterfaceOriginKind {
31        InterfaceOriginKind::Configured
32    }
33
34    fn fill_entropy(&mut self, bytes: &mut [u8]);
35
36    /// The storage the frame being received accumulates in — the seam's granted inbound slot, so a streaming deframer's writes land once, already across the seam. Parks until a slot is free (backpressure: an interface that cannot grant stops reading its medium). Repeated calls before [`commit_inbound`](Self::commit_inbound) return the same storage with its accumulation intact, so one frame may arrive across many reads.
37    async fn inbound_sink(&mut self) -> &mut dyn FrameSink;
38
39    /// Hand the manifold the frame accumulated in [`inbound_sink`](Self::inbound_sink) and release the storage. An empty sink commits nothing — delimiter-only keepalives die here, in one place, for every interface.
40    async fn commit_inbound(&mut self);
41
42    /// Hand the manifold one whole frame heard on the medium — the datagram path, derived from the sink pair. A frame past the sink's capacity is dropped whole.
43    async fn next_inbound(&mut self, frame: &[u8]) {
44        let sink = self.inbound_sink().await;
45        sink.clear();
46        if sink.extend_from_slice(frame).is_err() {
47            return;
48        }
49        self.commit_inbound().await;
50    }
51
52    async fn next_inbound_with_phy(&mut self, frame: &[u8], _phy: PacketPhyStats) {
53        self.next_inbound(frame).await;
54    }
55
56    async fn next_outbound(&mut self) -> &[u8];
57
58    /// Accept responsibility for a frame copied into the interface's own
59    /// bounded pending storage, allowing the seam-owned slot to be reused.
60    /// This is not completion; [`complete_outbound`](Self::complete_outbound)
61    /// still reports the eventual disposition.
62    fn accept_outbound_custody(&mut self) {}
63
64    fn complete_outbound(&mut self, _disposition: OutboundDisposition) {}
65
66    /// A further frame already committed for this interface, if one is waiting. Never parks; the borrow contract matches [`next_outbound`](Self::next_outbound). Serve loops use it to coalesce a burst that queued behind the frame being written into one wire write; the default never offers one, so a seam without it simply never batches.
67    fn try_next_outbound(&mut self) -> Option<&[u8]> {
68        None
69    }
70
71    async fn request_tunnel_synthesis(&mut self) {}
72}
73
74#[allow(async_fn_in_trait)]
75pub trait Interface {
76    const HW_MTU: usize;
77
78    /// The medium this interface speaks, which is also the namespace root of its id ([`from_channel_tag`](crate::interfaces::InterfaceId::from_channel_tag)).
79    const KIND: InterfaceKind;
80
81    /// There is one hard contract for `channel_tag()`: distinct bytes across distinct concurrent communication channels, and the same bytes across every reconnect and reboot for that effective channel.
82    ///
83    /// The tag names *which* channel of this medium the interface is: e.g., a TCP `host:port`, a BLE peer MAC, a LoRa frequency + modulation profile. [`InterfaceId::from_channel_tag`](crate::interfaces::InterfaceId::from_channel_tag) hashes it into this interface's id (`[KIND] ++ sha256(tag)[..7]`), the engine's entire notion of the interface: routes, links, and the departure grace all key on it. Same tag means the re-attached interface is the old one and its routes survive; a shared tag would fuse two distinct live channels into the same id.
84    fn channel_tag(&self) -> &[u8];
85
86    fn descriptor(&self) -> InterfaceDescriptor;
87
88    async fn run<S: InterfaceSeam>(self, seam: S);
89}