Skip to main content

lxmf_embedded_mini/
runtime.rs

1use heapless::{Deque, Vec};
2
3use crate::{
4    wire::WIRE_HEADER_LENGTH, LinkState, MiniError, MiniMessage, MiniResult, MiniStore,
5    MiniTransport, HASH_LENGTH,
6};
7
8const RECENT_REPLAY_SOURCES: usize = 8;
9
10#[derive(Debug, Clone, Copy, Eq, PartialEq)]
11pub struct MiniRuntimeConfig {
12    pub local_identity: [u8; HASH_LENGTH],
13}
14
15#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
16pub struct MiniRuntimeStats {
17    pub queued: u32,
18    pub sent: u32,
19    pub deferred: u32,
20    pub received: u32,
21    pub replay_rejected: u32,
22    pub event_overflows: u32,
23}
24
25#[derive(Debug, Clone, Copy, Eq, PartialEq)]
26pub struct RuntimeStatus {
27    pub outbound_len: usize,
28    pub event_len: usize,
29    pub replay_floor: u64,
30    pub stats: MiniRuntimeStats,
31}
32
33#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34pub enum MiniEvent {
35    Bootstrapped { replay_floor: u64 },
36    MessageQueued { sequence: u64, bytes: usize },
37    FrameSent { sequence: u64, bytes: usize },
38    FrameDeferred { sequence: u64, error: MiniError },
39    MessageReceived { sequence: u64, source: [u8; HASH_LENGTH], bytes: usize },
40    FrameRejected { sequence: u64, error: MiniError },
41}
42
43#[derive(Debug, Clone, Eq, PartialEq)]
44struct OutboundFrame<const FRAME: usize> {
45    sequence: u64,
46    bytes: Vec<u8, FRAME>,
47}
48
49#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50struct ReplaySourceFloor {
51    source: [u8; HASH_LENGTH],
52    floor: u64,
53}
54
55#[derive(Debug, Clone)]
56pub struct MiniNodeRuntime<
57    const TITLE: usize,
58    const CONTENT: usize,
59    const FRAME: usize,
60    const OUTBOUND: usize,
61    const EVENTS: usize,
62> {
63    config: MiniRuntimeConfig,
64    replay_floor: u64,
65    next_sequence: u64,
66    bootstrapped: bool,
67    outbound: Deque<OutboundFrame<FRAME>, OUTBOUND>,
68    events: Deque<MiniEvent, EVENTS>,
69    replay_source_floors: Vec<ReplaySourceFloor, RECENT_REPLAY_SOURCES>,
70    stats: MiniRuntimeStats,
71}
72
73impl<
74        const TITLE: usize,
75        const CONTENT: usize,
76        const FRAME: usize,
77        const OUTBOUND: usize,
78        const EVENTS: usize,
79    > MiniNodeRuntime<TITLE, CONTENT, FRAME, OUTBOUND, EVENTS>
80{
81    pub fn new(config: MiniRuntimeConfig) -> MiniResult<Self> {
82        if config.local_identity == [0; HASH_LENGTH] || OUTBOUND == 0 || EVENTS == 0 {
83            return Err(MiniError::InvalidInput);
84        }
85        Ok(Self {
86            config,
87            replay_floor: 0,
88            next_sequence: 1,
89            bootstrapped: false,
90            outbound: Deque::new(),
91            events: Deque::new(),
92            replay_source_floors: Vec::new(),
93            stats: MiniRuntimeStats::default(),
94        })
95    }
96
97    pub fn queue_message(
98        &mut self,
99        destination: [u8; HASH_LENGTH],
100        title: &[u8],
101        content: &[u8],
102        signature: [u8; 64],
103        timestamp: f64,
104    ) -> MiniResult<u64> {
105        if self.outbound.len() >= OUTBOUND {
106            return Err(MiniError::Backpressure);
107        }
108        self.ensure_event_capacity()?;
109
110        let sequence = self.next_sequence;
111        let message = MiniMessage::<TITLE, CONTENT>::new(
112            destination,
113            self.config.local_identity,
114            signature,
115            timestamp,
116            title,
117            content,
118        )?;
119        let mut bytes = Vec::<u8, FRAME>::new();
120        bytes.resize_default(message.encoded_len()?).map_err(|_| MiniError::CapacityExceeded)?;
121        let written = message.encode(&mut bytes)?;
122        bytes.truncate(written);
123        let queued_bytes = bytes.len();
124        self.outbound
125            .push_back(OutboundFrame { sequence, bytes })
126            .map_err(|_| MiniError::Backpressure)?;
127        self.next_sequence = self.next_sequence.saturating_add(1);
128        self.stats.queued = self.stats.queued.saturating_add(1);
129        self.push_event(MiniEvent::MessageQueued { sequence, bytes: queued_bytes })?;
130        Ok(sequence)
131    }
132
133    pub fn tick<T: MiniTransport, S: MiniStore>(
134        &mut self,
135        transport: &mut T,
136        store: &mut S,
137        scratch: &mut [u8],
138    ) -> MiniResult<()> {
139        if !self.bootstrapped {
140            self.replay_floor = store.load_replay_floor(&self.config.local_identity)?;
141            self.bootstrapped = true;
142            self.push_event(MiniEvent::Bootstrapped { replay_floor: self.replay_floor })?;
143        }
144
145        self.poll_inbound(transport, store, scratch)?;
146        self.flush_outbound(transport)?;
147        Ok(())
148    }
149
150    pub fn poll_event(&mut self) -> Option<MiniEvent> {
151        self.events.pop_front()
152    }
153
154    pub fn status(&self) -> RuntimeStatus {
155        RuntimeStatus {
156            outbound_len: self.outbound.len(),
157            event_len: self.events.len(),
158            replay_floor: self.replay_floor,
159            stats: self.stats,
160        }
161    }
162
163    fn poll_inbound<T: MiniTransport, S: MiniStore>(
164        &mut self,
165        transport: &mut T,
166        store: &mut S,
167        scratch: &mut [u8],
168    ) -> MiniResult<()> {
169        while let Some(len) = transport.poll_frame(scratch)? {
170            let message = MiniMessage::<TITLE, CONTENT>::decode(&scratch[..len])?;
171            let sequence = message_sequence(&message);
172            if sequence <= self.replay_floor_for(&message.source) {
173                self.stats.replay_rejected = self.stats.replay_rejected.saturating_add(1);
174                self.push_event(MiniEvent::FrameRejected {
175                    sequence,
176                    error: MiniError::ReplayRejected,
177                })?;
178                continue;
179            }
180
181            self.note_replay_floor(message.source, sequence);
182            if sequence > self.replay_floor {
183                self.replay_floor = sequence;
184                // Persist a coarse high-water mark for compatibility, while
185                // replay rejection in the live runtime is scoped per source.
186                store.save_replay_floor(&self.config.local_identity, self.replay_floor)?;
187            }
188            self.stats.received = self.stats.received.saturating_add(1);
189            self.push_event(MiniEvent::MessageReceived {
190                sequence,
191                source: message.source,
192                bytes: message.content.len(),
193            })?;
194        }
195        Ok(())
196    }
197
198    fn flush_outbound<T: MiniTransport>(&mut self, transport: &mut T) -> MiniResult<()> {
199        if transport.link_state() != LinkState::Up {
200            return Ok(());
201        }
202
203        while let Some(frame) = self.outbound.front() {
204            let sequence = frame.sequence;
205            match transport.send_frame(&frame.bytes) {
206                Ok(()) => {
207                    let sent = self.outbound.pop_front().ok_or(MiniError::InvalidInput)?;
208                    self.stats.sent = self.stats.sent.saturating_add(1);
209                    self.push_event(MiniEvent::FrameSent {
210                        sequence: sent.sequence,
211                        bytes: sent.bytes.len(),
212                    })?;
213                }
214                Err(error @ (MiniError::Backpressure | MiniError::Disconnected)) => {
215                    self.stats.deferred = self.stats.deferred.saturating_add(1);
216                    self.push_event(MiniEvent::FrameDeferred { sequence, error })?;
217                    break;
218                }
219                Err(error) => {
220                    let dropped = self.outbound.pop_front().ok_or(MiniError::InvalidInput)?;
221                    self.stats.deferred = self.stats.deferred.saturating_add(1);
222                    self.push_event(MiniEvent::FrameDeferred {
223                        sequence: dropped.sequence,
224                        error,
225                    })?;
226                }
227            }
228        }
229        Ok(())
230    }
231
232    fn ensure_event_capacity(&mut self) -> MiniResult<()> {
233        if self.events.len() >= EVENTS {
234            self.stats.event_overflows = self.stats.event_overflows.saturating_add(1);
235            return Err(MiniError::EventOverflow);
236        }
237        Ok(())
238    }
239
240    fn push_event(&mut self, event: MiniEvent) -> MiniResult<()> {
241        self.ensure_event_capacity()?;
242        self.events.push_back(event).map_err(|_| MiniError::EventOverflow)
243    }
244
245    fn replay_floor_for(&self, source: &[u8; HASH_LENGTH]) -> u64 {
246        self.replay_source_floors
247            .iter()
248            .find(|entry| &entry.source == source)
249            .map(|entry| entry.floor)
250            .unwrap_or(0)
251    }
252
253    fn note_replay_floor(&mut self, source: [u8; HASH_LENGTH], floor: u64) {
254        if let Some(entry) =
255            self.replay_source_floors.iter_mut().find(|entry| entry.source == source)
256        {
257            entry.floor = floor;
258            return;
259        }
260
261        if self.replay_source_floors.len() >= RECENT_REPLAY_SOURCES {
262            self.replay_source_floors.remove(0);
263        }
264        let _ = self.replay_source_floors.push(ReplaySourceFloor { source, floor });
265    }
266}
267
268fn message_sequence<const TITLE: usize, const CONTENT: usize>(
269    message: &MiniMessage<TITLE, CONTENT>,
270) -> u64 {
271    message.timestamp
272}
273
274pub const fn minimum_frame_capacity(title: usize, content: usize) -> usize {
275    WIRE_HEADER_LENGTH + 1 + 9 + 5 + title + 5 + content + 1
276}