ReactiveMap<K, V>
A collection whose keys are decided at run time. Each entry is stored at its own
path, so a change is observable per entry as well as for the whole - which is
what separates it from keeping a HashMap in one field.
Reading
Section titled “Reading”let one: Option<u64> = widths.get("cpu");
if let Some(width) = widths.get("cpu") { println!("cpu is {width}");}
let there = widths.contains_key("mem");let how_many = widths.len();
for key in widths.keys() { println!("{key}");}
for (key, value) in widths.entries() { println!("{key}: {value}");}len is answered from the map’s own projection, so it is a counter read rather
than a scan, and it counts writes that have not reached disk yet.
keys and entries walk that projection one entry at a time. keys touches no
values at all - nothing beside a key is deserialized.
Looking without taking
Section titled “Looking without taking”entries clones what it hands out, because Iterator cannot lend from the
thing it is iterating. view lends instead: bind it, then iter(), and nothing
is cloned that you do not clone yourself.
let held = widths.view();let total: u64 = held.iter().map(|(_, width)| width).sum();
let mut widest = 0;for (_, width) in &widths.view() { widest = widest.max(*width);}A for keeps the temporary alive for the whole loop, so the binding is only
needed when what you read outlives the loop.
A walk owns a version
Section titled “A walk owns a version”view, entries and keys each take the map’s current version and read from
that. Nothing is locked while one is alive, so a walk neither waits for a writer
nor keeps one waiting, whatever thread either of them is on.
Writing to the map during a walk is ordinary, the walking thread included:
let mut walked = Vec::new();
for (key, width) in widths.entries() { widths.remove(&key)?; walked.push((key, width));}
assert_eq!(walked, [("cpu".to_string(), 120), ("mem".to_string(), 80)]);assert!(widths.is_empty());Every key is still offered, because the walk reads the version it started on and the removals go to the map. A write that lands during a walk is in the next read rather than that one.
That is the thing to know when a walk is kept rather than finished. A view
bound for a frame holds what the map held when it was taken, so two reads made
from it agree with each other even if the map moved between them. Holding it
costs the version it pins, which is what the map has dropped since - not a copy
of the map.
Writing
Section titled “Writing”widths.insert("cpu".to_string(), &120)?;
widths.update("cpu", &200)?;widths.modify("cpu", |width| *width += 10)?;
let absent = widths.update("gpu", &90);
widths.remove("cpu")?;widths.clear()?;insert adds a key or replaces it. update writes a key that is already
there and fails with Absent when it is not.
The reason is what subscribers are handed. MapChange::Update carries the
previous value, and a key that does not exist has none, so update on an absent
key could only announce a change it was unable to describe.
modify is update reached without rebuilding the value, which matters when it
is a large struct.
remove sends one MapChange::Remove carrying this handle’s id, and one store
write. clear is the other shape: one event, one delete, whatever the map held.
To drop a subset, walk and remove - the walk reads its own version, so the loop is the plain one:
for (key, width) in widths.entries() { if width < 80 { widths.remove(&key)?; }}What a key may be
Section titled “What a key may be”A key is the name its entry sits at. That is what ReactiveMapKey asks for -
AsRef<str> - so a String key is spelled by being itself: nothing is rendered
on a lookup and nothing is parsed on the way back.
A key that is not a string goes through Id, which renders it once and keeps
the rendering:
let ports = store.kv().map::<Id<u16>, bool>("ports")?;
ports.insert(Id::new(8080), &true)?;assert_eq!(ports.get("8080"), Some(true));Id::new spells the value when the key is made rather than at every read, which
is what a Uuid wants most: 36 characters do not fit beside the value, so
spelling one reaches the heap, and this pays that once for the life of the key.
get hands back what was spelled.
The empty name is a name. Every format this library writes lets a member be named with nothing, so a map holds such an entry, lists it and writes to it like any other.
What order entries come back in
Section titled “What order entries come back in”Sorted by the name each key borrows, which is the order the store lists in - an entry is one level under the map, and a store orders keys by their levels:
counts.insert("9".to_string(), &1)?;counts.insert("10".to_string(), &1)?;counts.insert("a.b".to_string(), &1)?;counts.insert("a1b".to_string(), &1)?;
let order: Vec<String> = counts.keys().collect();assert_eq!(order, ["10", "9", "a.b", "a1b"]);Numbers spelled as text sort as text, so "10" comes before "9" - and an
Id<u16> sorts the same way, by its spelling rather than by the number. A name
holding the separator sorts by the name: a.b comes before a1b, because .
is below 1. The escape the store writes around that separator is a spelling,
and nothing compares paths by their spelling.
Insertion order is not recorded anywhere. If the order matters - table columns, steps in a list - keep that order yourself and use the map for lookup.
Hearing about a change
Section titled “Hearing about a change”let _all = widths.subscribe_any(|change| { println!("{change:?}");});
let _cpu = widths.subscribe_key("cpu".to_string(), |change| { println!("cpu: {change:?}");});
widths.insert("cpu".to_string(), &120)?;widths.insert("mem".to_string(), &80)?;subscribe_any fires for every change to any key; subscribe_key narrows to
one. Both return a guard, and dropping it ends the subscription.
subscription_with() is the same with the links: .key(..) to narrow,
.external() to skip your own writes, .stream() to take the changes into a
loop of your own instead of a callback. See
Subscriptions.
What external filters
Section titled “What external filters”What external is, and the handle ids it decides by, is
Subscriptions’s
subject. What a map does differently is this.
On a map it filters Update and nothing else. Insert, Remove and Clear
reach every subscriber including the one that caused them.
The line is between editing a value and changing what the map holds. A value you wrote yourself is your own business. A key appearing or disappearing changes the shape of the map, and a view listing the keys has to rebuild whether or not it was the one that added the key.
That has a consequence worth knowing: insert on a new key is an Insert and
on an existing one an Update, so whether your own call comes back to you
depends on whether the key was already there.
One entry as a cell
Section titled “One entry as a cell”entry_cell(key) gives a ReactiveCell
onto one entry, for handing a single value somewhere that should not know about
the map.