refactor(core): narrow the 1.0 public API

This commit is contained in:
2026-10-22 08:44:09 -07:00
parent 94c3a3c8ad
commit 158689e265
18 changed files with 166 additions and 95 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ use crate::observation::ObservationId;
/// A fixed-size salted Bloom filter keyed by `ObservationId`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Bloom {
pub salt: u32,
pub(crate) struct Bloom {
pub(crate) salt: u32,
bits: [u8; consts::SUMMARY_BYTES],
/// Bytes in use: a power of two in `1..=SUMMARY_BYTES`.
len: usize,
+2 -2
View File
@@ -5,7 +5,7 @@
/// Wire envelope version byte prepended to every serialized `Frame`.
///
/// The first byte of every frame on the wire; [`crate::wire::Frame::decode`]
/// The first byte of every frame on the wire; `crate::wire::Frame::decode`
/// rejects any frame whose leading byte differs from this
/// (`WireError::VersionMismatch`), and [`crate::wire::frame_sender`] skips it.
/// `4` is the first value in the `4..=0x7F` range that fits a single-byte
@@ -42,7 +42,7 @@ pub const REASSEMBLY_TIMEOUT_MS: u64 = 5000;
pub const SEND_QUEUE_CAP: usize = 256;
/// Minimum usable UDP frame size in bytes (the BLE floor below which a
/// fragment header leaves no room for payload). [`crate::frag::fragment`] gives
/// fragment header leaves no room for payload). `crate::frag::fragment` gives
/// up cleanly with `WireError::TooLarge` for any `mtu` below this rather than
/// emitting a truncated frame; the MTU sweep runs from here to [`MAX_FRAME`].
pub const MIN_MTU: usize = 23;
+17 -13
View File
@@ -1,6 +1,6 @@
//! Exchange state machine for tines-core.
//!
//! [`Exchange`] tracks the peers this node knows about and drives the
//! `Exchange` tracks the peers this node knows about and drives the
//! content-defined gossip protocol: acknowledging received observations,
//! summarizing what we hold, requesting what peers are missing, and delivering
//! observations (fragmenting them across the wire when they exceed the MTU).
@@ -13,8 +13,10 @@
use crate::consts;
use crate::{
Bloom, Event, Frame, NodeId, Observation, ObservationId, Reassembler, SendError, Store, Tick,
WireError,
bloom::Bloom,
frag::Reassembler,
wire::{Frame, WireError},
Event, NodeId, Observation, ObservationId, SendError, Store, Tick,
};
use heapless::Vec;
@@ -31,7 +33,7 @@ const SUMMARY_OVERHEAD: usize = 25;
/// MTU changed). The stream is then abandoned and restarted rather than
/// sending bytes the receiver cannot reassemble.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InFlight {
pub(crate) struct InFlight {
/// The observation being fragmented.
pub id: ObservationId,
/// Index of the next fragment to send.
@@ -45,8 +47,8 @@ pub struct InFlight {
pub len: usize,
}
/// Why the [`Exchange`] dropped an inbound frame it could not use; reported
/// via [`NodeEvent::Dropped`]. A peer's [`Exchange`] rejects expired addressed
/// Why the `Exchange` dropped an inbound frame it could not use; reported
/// via [`NodeEvent::Dropped`]. A peer's `Exchange` rejects expired addressed
/// observations by policy — those are *not* counted as drops.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -57,7 +59,7 @@ pub enum DropReason {
/// The version byte read off the wire.
version: u8,
},
/// The frame was not a valid [`Frame`] (truncated, garbage, or empty).
/// The frame was not a valid `Frame` (truncated, garbage, or empty).
Malformed,
}
@@ -91,7 +93,7 @@ pub enum NodeEvent<A> {
},
}
/// The [`Exchange`]'s counted-drop table: up to [`consts::DROP_TABLE_CAP`]
/// The `Exchange`'s counted-drop table: up to [`consts::DROP_TABLE_CAP`]
/// distinct `(addr, DropReason)` pairs, each with a saturating count, plus a
/// single overflow counter. Counters are drained — and reset — through
/// [`Exchange::drain_events`].
@@ -163,8 +165,8 @@ impl<A: Clone + PartialEq> DropTable<A> {
}
}
/// A single peer the [`Exchange`] is tracking.
pub struct Peer<A> {
/// A single peer the `Exchange` is tracking.
pub(crate) struct Peer<A> {
/// Wire address of the peer.
pub addr: A,
/// Node id learned from the first frame received from this peer.
@@ -266,7 +268,7 @@ struct UndoAck<A> {
}
/// The exchange state machine, tracking up to [`consts::MAX_PEERS`] peers.
pub struct Exchange<A> {
pub(crate) struct Exchange<A> {
me: NodeId,
salt: u32,
peers: Vec<Peer<A>, { consts::MAX_PEERS }>,
@@ -763,12 +765,14 @@ impl<A: Clone + PartialEq + core::fmt::Debug> Exchange<A> {
Some((addr, n))
}
// Only reached from alloc-gated tests, so unused under --no-default-features.
#[allow(dead_code)]
/// Returns the current peers.
pub fn peers(&self) -> &[Peer<A>] {
&self.peers
}
/// [`Exchange`]s emit [`NodeEvent`]s — confirmed deliveries plus counted
/// `Exchange`s emit [`NodeEvent`]s — confirmed deliveries plus counted
/// drops — through `Exchange::drain_events`; `tines` surfaces them to its
/// transport, so the drain method is unused inside `tines-core` itself.
///
@@ -3103,7 +3107,7 @@ mod tests {
}
/// A frame that passes the version gate (byte 0 is WIRE_VERSION) but whose
/// body isn't a valid postcard [`Frame`] is a counted malformed drop.
/// body isn't a valid postcard `Frame` is a counted malformed drop.
#[test]
fn malformed_is_counted() {
let mut store = MemStore::new();
+9 -4
View File
@@ -1,11 +1,14 @@
//! Fragmentation and reassembly of `Observation`s over a byte-oriented link.
use crate::consts;
use crate::{Frame, NodeId, Observation, ObservationId, Tick, WireError};
use crate::{
wire::{Frame, WireError},
NodeId, Observation, ObservationId, Tick,
};
use heapless::Vec;
/// Header allowance per fragment (bytes), pinned.
pub const FRAG_OVERHEAD: usize = 65;
pub(crate) const FRAG_OVERHEAD: usize = 65;
/// The most fragments one observation may be split into: the reassembler
/// tracks received indices in a `u64` bitset.
@@ -27,9 +30,11 @@ pub(crate) fn plan(len: usize, mtu: usize) -> Option<(usize, usize)> {
Some((chunk, count))
}
// Only reached from alloc-gated tests, so unused under --no-default-features.
#[allow(dead_code)]
/// Encodes `obs` as one Obs frame if it fits in `mtu`, else as N Frag frames.
/// Returns the number of frames; writes each via `emit(frame_bytes)`.
pub fn fragment(
pub(crate) fn fragment(
from: NodeId,
obs: &Observation,
mtu: usize,
@@ -74,7 +79,7 @@ pub fn fragment(
}
/// A single-slot reassembler for inbound Frag streams.
pub struct Reassembler {
pub(crate) struct Reassembler {
id: Option<ObservationId>,
count: u8,
received: u64,
+6 -8
View File
@@ -5,29 +5,27 @@
extern crate alloc;
pub mod belief;
pub mod bloom;
mod bloom;
pub mod consts;
pub mod exchange;
pub mod frag;
mod exchange;
mod frag;
#[cfg(feature = "alloc")]
pub mod mem_store;
pub mod node;
pub mod observation;
pub mod store;
pub mod transport;
pub mod wire;
mod wire;
pub use belief::{BeliefPolicy, LatestWins, SelfFirst};
pub use bloom::Bloom;
pub use exchange::{DropReason, Exchange, NodeEvent, Peer};
pub use frag::{fragment, Reassembler};
pub use exchange::{DropReason, NodeEvent};
pub use node::{Node, ObserveError};
pub use observation::{
Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic,
};
pub use store::{Store, StoreError, TrimPolicy};
pub use transport::{Event, FrameBuf, SendError, Transport};
pub use wire::{Frame, WireError};
pub use wire::frame_sender;
#[cfg(feature = "alloc")]
pub use mem_store::MemStore;
+8 -7
View File
@@ -1,21 +1,22 @@
//! Node facade for tines-core.
//!
//! [`Node`] is the single entry point that ties the [`Store`], the
//! [`Exchange`] state machine, and a [`Transport`] together behind the two
//! `Exchange` state machine, and a [`Transport`] together behind the two
//! operations an application needs: [`Node::observe`] to record an observation,
//! and [`Node::poll`] to drive the exchange.
//!
//! The facade never touches the heap: its only owned buffer, `out`, is a fixed
//! `[u8; MAX_FRAME]`. It also panics on nothing — oversized topics/payloads
//! become [`ObserveError::TooLarge`] rather than truncating, and malformed
//! transport frames are dropped by the [`Exchange`].
//! transport frames are dropped by the `Exchange`.
use crate::{
consts, BeliefPolicy, Dest, Exchange, NodeEvent, NodeId, Observation, ObservationId, Payload,
Store, StoreError, Tick, Topic, Transport, TrimPolicy,
consts, exchange::Exchange, BeliefPolicy, Dest, NodeEvent, NodeId, Observation, ObservationId,
Payload, Store, StoreError, Tick, Topic, Transport, TrimPolicy,
};
/// Errors returned by [`Node::observe`].
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum ObserveError {
/// The topic or payload exceeded `MAX_TOPIC`/`MAX_PAYLOAD`. The
@@ -105,7 +106,7 @@ impl<S: Store, T: Transport> Node<S, T> {
///
/// The resulting observation has `dest = Some(Dest { to, lifetime_ms })`,
/// so the exchange will deliver it only
/// to `to` (see [`Exchange`]). Returns the observation's content-derived
/// to `to` (see `Exchange`). Returns the observation's content-derived
/// id. An oversized topic or payload is rejected with
/// [`ObserveError::TooLarge`] rather than truncated; a store rejection is
/// surfaced as [`ObserveError::Store`]. Duplicate ids are accepted (the id
@@ -143,7 +144,7 @@ impl<S: Store, T: Transport> Node<S, T> {
/// Returns the number of *new* observations stored this round. If the
/// transport refuses the frame with `Busy` or `Unreachable`, the exchange
/// rolls back and offers the same frame on a later poll; a `TooLarge`
/// frame is dropped (see [`Exchange::on_send_error`]).
/// frame is dropped (see `Exchange::on_send_error`).
pub fn poll(&mut self, now: Tick) -> usize {
let mut stored = 0usize;
while let Some(ev) = self.transport.poll() {
@@ -165,7 +166,7 @@ impl<S: Store, T: Transport> Node<S, T> {
stored
}
/// Drains the [`NodeEvent`]s the [`Exchange`] has collected: confirmed
/// Drains the [`NodeEvent`]s the `Exchange` has collected: confirmed
/// deliveries first (oldest-first), then one [`NodeEvent::Dropped`] per
/// counted drop.
///
+5
View File
@@ -35,6 +35,11 @@ pub struct Dest {
}
/// An observation published by a node, with content-derived identity.
///
/// `#[non_exhaustive]`: a future 1.x may add fields, so build it via the
/// constructors on [`Observation`] rather than a struct literal outside
/// `tines-core`.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Observation {
pub observer: NodeId,
+5
View File
@@ -13,6 +13,10 @@ use crate::observation::{Observation, ObservationId, Tick};
/// Rules are applied in the order listed here: per-topic retention, then a
/// global age cutoff, then a global count cap. Any field left as `None` is
/// ignored.
///
/// `#[non_exhaustive]`: build via [`TrimPolicy::default`] and set fields, so a
/// future 1.x can add policy knobs without a breaking change.
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct TrimPolicy {
/// Keep the `N` youngest (lowest current age) observations per exact topic.
@@ -24,6 +28,7 @@ pub struct TrimPolicy {
}
/// Errors returned by [`Store`] operations.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum StoreError {
/// The store is full and cannot accept new observations.
+2
View File
@@ -15,6 +15,7 @@ pub type FrameBuf = heapless::Vec<u8, { consts::MAX_FRAME }>;
/// `Contact`/`Lost`; the `large_enum_variant` lint is intentionally suppressed
/// to keep `FrameBuf` off the heap in the `no_std` core.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum Event<A> {
/// A peer became reachable.
@@ -26,6 +27,7 @@ pub enum Event<A> {
}
/// Reasons a `Transport::send` call can fail.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum SendError {
/// The transport is busy with an in-flight send and cannot accept another.
+3 -3
View File
@@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
/// large, so the informative `large_enum_variant` lint is suppressed here.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum Frame {
pub(crate) enum Frame {
Summary {
from: NodeId,
salt: u32,
@@ -85,7 +85,7 @@ impl Frame {
/// Peek the [`NodeId`] a wire frame was sent by, without fully decoding it.
///
/// A [`Frame`] envelope is a `WIRE_VERSION` byte, then an externally-tagged
/// A `Frame` envelope is a `WIRE_VERSION` byte, then an externally-tagged
/// postcard enum: `bytes[0]` is the version, `bytes[1]` the variant tag, and
/// `from` (the first field) occupies the 16 bytes at `bytes[2..18]`. This skips
/// the two leading bytes, reads those 16 bytes, and returns `None` for anything
@@ -114,7 +114,7 @@ pub fn frame_sender(bytes: &[u8]) -> Option<NodeId> {
/// Errors produced while (de)serializing frames on the wire.
#[derive(Debug, PartialEq, Eq)]
pub enum WireError {
pub(crate) enum WireError {
/// The output buffer was too small to hold the serialized frame.
TooLarge,
/// The input bytes were malformed, truncated, or not a valid `Frame`.
+26
View File
@@ -0,0 +1,26 @@
//! Compile probe for the tines-core 1.0 public surface (Task 5).
//!
//! Every `use` path here must keep resolving against `tines_core`. A future
//! change that removes any of these items from the public API — or drops a
//! crate-root re-export that Task 5 established — fails to build here, which
//! is exactly what the probe guards against. CI compiles it on the workspace
//! both with and without the default (alloc) features.
#![allow(unused_imports)]
// Re-exported at the crate root by Task 5's `tines_core::lib`.
use tines_core::consts::WIRE_VERSION;
use tines_core::{
belief::{BeliefPolicy, LatestWins, SelfFirst},
node::{Node, ObserveError},
observation::{
Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic,
},
store::{Store, StoreError, TrimPolicy},
transport::{Event, FrameBuf, SendError, Transport},
DropReason, NodeEvent,
};
use tines_core::{consts, frame_sender};
// `MemStore` lives behind the `alloc` feature.
#[cfg(feature = "alloc")]
use tines_core::MemStore;