feat(core): Store::release and is_released (0.3.0)
This commit is contained in:
@@ -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
|
[Semantic Versioning](https://semver.org/); before 1.0, a minor bump may break
|
||||||
the API.
|
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
|
## 0.2.1 — 2026-10-18
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+2
-2
@@ -548,7 +548,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tines-core"
|
name = "tines-core"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blake2",
|
"blake2",
|
||||||
"heapless",
|
"heapless",
|
||||||
@@ -558,7 +558,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tines-sim"
|
name = "tines-sim"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proptest",
|
"proptest",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ members = ["tines-core", "tines-sim"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
authors = ["K. Isom"]
|
authors = ["K. Isom"]
|
||||||
|
|||||||
@@ -1820,6 +1820,12 @@ mod tests {
|
|||||||
fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
||||||
self.inner.for_each_addressed_id(f)
|
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)) {
|
fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation)) {
|
||||||
self.inner.for_each_topic(prefix, now, f)
|
self.inner.for_each_topic(prefix, now, f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use alloc::vec::Vec;
|
|||||||
struct Held {
|
struct Held {
|
||||||
obs: Observation,
|
obs: Observation,
|
||||||
stored_at: Tick,
|
stored_at: Tick,
|
||||||
|
released: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An unbounded [`Store`] backed by a `BTreeMap` keyed by content-derived
|
/// An unbounded [`Store`] backed by a `BTreeMap` keyed by content-derived
|
||||||
@@ -77,6 +78,7 @@ impl Store for MemStore {
|
|||||||
Held {
|
Held {
|
||||||
obs,
|
obs,
|
||||||
stored_at: now,
|
stored_at: now,
|
||||||
|
released: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@@ -112,12 +114,24 @@ impl Store for MemStore {
|
|||||||
|
|
||||||
fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
fn for_each_addressed_id(&self, f: &mut dyn FnMut(&ObservationId)) {
|
||||||
for (id, held) in self.map.iter() {
|
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);
|
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)) {
|
fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation)) {
|
||||||
for held in self.map.values() {
|
for held in self.map.values() {
|
||||||
if held.obs.topic.starts_with(prefix) {
|
if held.obs.topic.starts_with(prefix) {
|
||||||
@@ -438,4 +452,24 @@ mod tests {
|
|||||||
std::collections::BTreeSet::from_iter([0u64, 1, 2, 100])
|
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]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ pub trait Store {
|
|||||||
self.for_each_id(f)
|
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`
|
/// Visit every stored observation whose topic starts with `prefix`
|
||||||
/// (an empty prefix matches all), each age-adjusted to `now`.
|
/// (an empty prefix matches all), each age-adjusted to `now`.
|
||||||
fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation));
|
fn for_each_topic(&self, prefix: &[u8], now: Tick, f: &mut dyn FnMut(&Observation));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ keywords = ["robotics", "gossip", "simulation", "delay-tolerant"]
|
|||||||
categories = ["simulation", "science::robotics"]
|
categories = ["simulation", "science::robotics"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tines-core = { path = "../tines-core", version = "0.2.1" }
|
tines-core = { path = "../tines-core", version = "0.3.0" }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user