chore: release 0.2.0; document the store contract and on_send_error
CI / ci (push) Canceled after 0s

Bump tines-core and tines-sim to 0.2.0 (Peer::in_flight changed type).
Add CHANGELOG.md. README gains a "Writing a store" section and a note on
calling Exchange::on_send_error when driving Exchange without Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 05:24:30 -07:00
co-authored by Claude Opus 5.5
parent 203550bfc4
commit 5c8bfe89ac
5 changed files with 92 additions and 5 deletions
+57
View File
@@ -0,0 +1,57 @@
# Changelog
All notable changes to the tines crates. Versions follow
[Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break
the API.
## 0.2.0 — 2026-10-18
Hardening from a full-codebase review. Wire-compatible with 0.1.0 except
where noted.
### Added
- `Exchange::on_send_error`: rolls back the last `next_frame` after a failed
send. `Node::poll` calls it, so a `Busy` or `Unreachable` frame is retried
instead of being lost.
- `Store::for_each_addressed_id`, with a default that visits every id.
`MemStore` overrides it, so custody no longer reads every broadcast
observation on each poll.
- `Bloom::fold`, and `Bloom::from_parts` now accepts any power-of-two length up
to `SUMMARY_BYTES`. Summaries are folded to fit the transport's MTU; a
0.1.0 node rejects a folded summary, so mixed-version links only sync when
the MTU allows a full 512-byte filter.
- `InFlight`, describing a fragment stream in progress.
### Changed
- **Breaking:** `Peer::in_flight` is now `Option<InFlight>` (was
`Option<(ObservationId, u8)>`).
- Custody (addressed) and broadcast sending alternate per peer, and addressed
candidates are taken in rotation.
- Bounce suppression lasts one contact, as the addressed-delivery spec says.
### Fixed
- A fragment stream could panic, or send corrupt fragments, when the
observation's age changed encoded length mid-stream or the tick went
backwards.
- Summaries larger than the MTU stopped all broadcast sync on narrow links.
- A full bounce table silently stopped recording, so one addressed message
was re-sent on every poll and starved broadcast.
- An addressed send interrupted another peer's fragment stream, which was
then never finished.
- An addressed message that couldn't fit the MTU blocked every other
addressed message for that peer.
- A duplicate addressed message was forwarded back to the carrier that sent
it.
### CI
- Tests, clippy and docs also run without the `alloc` feature; doc warnings
fail the build; `--locked` everywhere; toolchain pinned to 1.99.0; actions
pinned to commit SHAs.
## 0.1.0
Initial release: `tines-core` and `tines-sim`.
Generated
+2 -2
View File
@@ -548,7 +548,7 @@ dependencies = [
[[package]]
name = "tines-core"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"blake2",
"heapless",
@@ -558,7 +558,7 @@ dependencies = [
[[package]]
name = "tines-sim"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"proptest",
"rand 0.8.8",
+30
View File
@@ -23,6 +23,7 @@ hear each other.
- [Addressed delivery](#addressed-delivery)
- [Usage](#usage)
- [Writing a transport](#writing-a-transport)
- [Writing a store](#writing-a-store)
- [The simulator](#the-simulator)
- [Crate layout](#crate-layout)
- [Building and CI](#building-and-ci)
@@ -350,6 +351,35 @@ pub trait Transport {
- **Report back-pressure honestly.** Return `SendError::Busy` when the radio
can't take a frame right now; the exchange will offer the same frame again
on the next poll. `TooLarge` tells the exchange to give that frame up.
- **Driving `Exchange` without `Node`?** `Exchange::next_frame` assumes the
frame it returns gets sent. If `send` fails, call
`Exchange::on_send_error(&err)` before the next `next_frame`, or the frame
is treated as delivered. `Node::poll` does this for you.
## Writing a store
`MemStore` (behind the default `alloc` feature) is the reference backend. A
flash or SQLite backend implements the `Store` trait:
| Method | Contract |
|-------------------------|--------------------------------------------------------------------------|
| `insert` | `Ok(true)` if new; `Ok(false)` for a known id, leaving the existing entry (and its age) untouched; `Err(StoreError::Full)` when out of room |
| `contains` | whether an id is stored |
| `get` | the observation with `age_ms` advanced by `now − stored_at`, saturating if `now` is earlier |
| `len` | number of stored observations |
| `for_each_id` | every stored id, any order |
| `for_each_broadcast_id` | ids with `to == None`; these feed the summary |
| `for_each_addressed_id` | ids with `to == Some(_)`; scanned for custody on every poll |
| `for_each_topic` | age-adjusted observations whose topic starts with a prefix |
| `trim` | apply a `TrimPolicy` (per-topic, then age, then count); return how many were removed |
The store must remember when each observation was inserted (`stored_at`, in
the caller's `Tick`s) so it can report current ages.
`for_each_addressed_id` has a default implementation that visits every id,
which is correct but makes the exchange read every broadcast observation on
each poll just to find the few addressed ones. Override it if your backend can
filter by `to` cheaply, e.g. with an index or a separate table.
## The simulator
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tines-core"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "Apache-2.0"
description = "Opportunistic shared memory for mostly-offline robots: a no_std core of content-addressed observations, gossip, and store-and-forward exchange."
+2 -2
View File
@@ -1,13 +1,13 @@
[package]
name = "tines-sim"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "Apache-2.0"
description = "Discrete-time simulator for tines: contact traces, convergence scenarios, and property tests."
authors = ["K. Isom"]
[dependencies]
tines-core = { path = "../tines-core", version = "0.1.0" }
tines-core = { path = "../tines-core", version = "0.2.0" }
rand = "0.8"
[dev-dependencies]