diff --git a/CHANGELOG.md b/CHANGELOG.md index 998859dc..b70c0da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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.3.0 — 2026-10-18 + +### Breaking + +- `Store` now requires `release(&mut self, id)` and + `is_released(&self, id) -> bool`. Implementations of `Store` — including test + doubles and future backends — must provide them. + +### Added + +- `Store::release` marks an observation as sent, so it is no longer offered for + exchange. `MemStore` tracks a per-entry `released` flag, and its + `for_each_addressed_id` skips released ids as an optimization (the exchange + predicate remains the load-bearing check). Releasing an id that is not stored + is a no-op. + ## 0.2.1 — 2026-10-18 ### Changed diff --git a/Cargo.lock b/Cargo.lock index 0adbfc8e..dc69b524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,7 +548,7 @@ dependencies = [ [[package]] name = "tines-core" -version = "0.2.1" +version = "0.3.0" dependencies = [ "blake2", "heapless", @@ -558,7 +558,7 @@ dependencies = [ [[package]] name = "tines-sim" -version = "0.2.1" +version = "0.3.0" dependencies = [ "proptest", "rand 0.8.8", diff --git a/Cargo.toml b/Cargo.toml index 43377637..1af11d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["tines-core", "tines-sim"] resolver = "2" [workspace.package] -version = "0.2.1" +version = "0.3.0" edition = "2021" license = "Apache-2.0" authors = ["K. Isom"] diff --git a/tines-core/src/exchange.rs b/tines-core/src/exchange.rs index fde08400..39604dbd 100644 --- a/tines-core/src/exchange.rs +++ b/tines-core/src/exchange.rs @@ -1820,6 +1820,12 @@ mod tests { fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) { self.inner.for_each_addressed_id(f) } + fn release(&mut self, id: &ObservationId) { + self.inner.release(id) + } + fn is_released(&self, id: &ObservationId) -> bool { + self.inner.is_released(id) + } fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation)) { self.inner.for_each_topic(prefix, now, f) } diff --git a/tines-core/src/mem_store.rs b/tines-core/src/mem_store.rs index 0034558b..a73d786d 100644 --- a/tines-core/src/mem_store.rs +++ b/tines-core/src/mem_store.rs @@ -21,6 +21,7 @@ use alloc::vec::Vec; struct Held { obs: Observation, stored_at: Tick, + released: bool, } /// An unbounded [`Store`] backed by a `BTreeMap` keyed by content-derived @@ -77,6 +78,7 @@ impl Store for MemStore { Held { obs, stored_at: now, + released: false, }, ); Ok(true) @@ -112,12 +114,24 @@ impl Store for MemStore { fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) { for (id, held) in self.map.iter() { - if held.obs.to.is_some() { + // Skipping released ids is an optimization; the exchange predicate + // is the load-bearing check. + if held.obs.to.is_some() && !held.released { f(id); } } } + fn release(&mut self, id: &ObservationId) { + if let Some(held) = self.map.get_mut(id) { + held.released = true; + } + } + + fn is_released(&self, id: &ObservationId) -> bool { + self.map.get(id).map(|held| held.released).unwrap_or(false) + } + fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation)) { for held in self.map.values() { if held.obs.topic.starts_with(prefix) { @@ -438,4 +452,24 @@ mod tests { std::collections::BTreeSet::from_iter([0u64, 1, 2, 100]) ); } + + #[test] + fn release_marks_and_is_released_reports() { + let mut s = MemStore::new(); + let o = obs([1; 16], 1, 0, None, b"door/1"); + let id = o.id(); + assert!(s.insert(o, 1000).unwrap()); + assert!(!s.is_released(&id)); + s.release(&id); + assert!(s.is_released(&id)); + } + + #[test] + fn release_unknown_is_noop() { + let mut s = MemStore::new(); + // Releasing an id that was never inserted must not panic... + s.release(&[0u8; 16]); + // ...and must not report anything as released. + assert!(!s.is_released(&[0u8; 16])); + } } diff --git a/tines-core/src/store.rs b/tines-core/src/store.rs index f93adce9..abc25b11 100644 --- a/tines-core/src/store.rs +++ b/tines-core/src/store.rs @@ -74,6 +74,17 @@ pub trait Store { self.for_each_id(f) } + /// Mark the observation with `id` as released: it has been sent and must + /// not be offered for exchange again. + /// + /// Releasing an id that is not currently stored is a no-op. The mark + /// persists for the lifetime of the entry (until it is trimmed); a + /// re-inserted observation starts un-released. + fn release(&mut self, id: &ObservationId); + + /// Returns `true` if the observation with `id` has been released. + fn is_released(&self, id: &ObservationId) -> bool; + /// Visit every stored observation whose topic starts with `prefix` /// (an empty prefix matches all), each age-adjusted to `now`. fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation)); diff --git a/tines-sim/Cargo.toml b/tines-sim/Cargo.toml index 7d00a44b..48cf75ee 100644 --- a/tines-sim/Cargo.toml +++ b/tines-sim/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "simulation", "delay-tolerant"] categories = ["simulation", "science::robotics"] [dependencies] -tines-core = { path = "../tines-core", version = "0.2.1" } +tines-core = { path = "../tines-core", version = "0.3.0" } rand = "0.8" [dev-dependencies]