Перейти к содержимому

ReactiveMap<K, V>

Коллекция, чьи ключи появляются в рантайме. Каждая запись лежит по своему пути, поэтому следить можно и за одной записью, и за всей картой сразу. Этим она и отличается от HashMap, засунутого в одно поле.

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 читает счётчик в собственной проекции карты — за одно обращение, вместе с записями, которые до диска ещё не дошли.

keys и entries идут по той же проекции, по одной записи за раз. keys значений не касается вовсе: кроме ключа не десериализуется ничего.

entries клонирует то, что отдаёт: Iterator не умеет одалживать из того, по чему идёт. view как раз одалживает — свяжите его, потом iter(), и склонируется только то, что вы склонируете сами.

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);
}

for держит временное значение живым весь цикл, так что связывать нужно только тогда, когда прочитанное переживёт цикл.

view, entries и keys берут текущую версию карты и читают из неё. Пока такой обход жив, ничего не заблокировано: он не ждёт пишущего и не заставляет ждать его, на каких бы потоках эти двое ни сидели.

Писать в карту во время обхода — обычное дело, в том числе с того самого потока, который по ней и идёт:

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());

Каждый ключ всё равно предложат: обход читает ту версию, с которой начался, а удаления уходят в карту. Запись, легшая посреди обхода, достанется следующему чтению, а не этому.

Знать это надо там, где обход не доводят до конца, а держат. view, связанный на кадр, показывает то, что карта держала в момент, когда его взяли, — поэтому два чтения из него сойдутся друг с другом, даже если карта между ними успела сдвинуться. Платите вы за него той версией, которую он закрепил: за то, что карта с тех пор отбросила, а не за копию карты.

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 добавляет ключ или заменяет его. update пишет в ключ, который уже есть, а если его нет — падает с Absent.

Причина — в том, что достаётся подписчикам. MapChange::Update несёт предыдущее значение, а у несуществующего ключа его взять негде: update по отсутствующему ключу объявил бы изменение, которое сам же и не может описать.

modify — тот же update, только значение не собирают заново. Это важно, когда значение — большая структура.

remove шлёт один MapChange::Remove с id этого хендла и одну запись в store. clear устроен иначе: одно событие и одно удаление, сколько бы карта ни держала.

Когда новое значение зависит от старого, а ключа может ещё не быть — счётчик, растущий список, — оба случая берёт на себя upsert, одной функцией:

for page in ["home", "about", "home"] {
hits.upsert(page, |seen| seen.map_or(1, |count| count + 1))?;
}
let now = hits
.durable()
.upsert("home", |seen| seen.map_or(1, |count| count + 1))?;

Функция получает Some со значением или None, а то, что она вернёт, записывается и возвращается вам. Ключ берётся взаймы, так что для ключа, который уже есть, вызывающему не нужно собирать String. Подписчики в первый раз видят Insert, потом Update — как у insert. Чтение и запись — два шага, как в modify: запись из другого потока, легшая между ними, будет затёрта.

Чтобы выбросить часть, обойдите и удалите — обход читает свою версию, поэтому цикл выходит самый обычный:

for (key, width) in widths.entries() {
if width < 80 {
widths.remove(&key)?;
}
}

Ключ — это имя, на котором сидит его запись. Ровно этого и просит ReactiveMapKey — AsRef<str>, — так что строковый ключ пишется собой: при поиске ничего не отрисовывается, при чтении с диска ничего не разбирается.

Нестроковый ключ идёт через Id, который отрисовывает значение один раз и держит написание при себе:

let ports = store.kv().map::<Id<u16>, bool>("ports")?;
ports.insert(Id::new(8080), &true)?;
assert_eq!(ports.get("8080"), Some(true));

Id::new пишет значение в момент постройки ключа, а не на каждом чтении, — и нужнее всего это Uuid: его 36 символов рядом со значением не помещаются, то есть написание стоит кучи, и так она платится один раз за всю жизнь ключа. get отдаёт обратно то, что было написано.

Пустое имя — тоже имя. Всякий формат, который пишет эта библиотека, разрешает члену называться ничем, поэтому такую запись карта держит, перечисляет и пишет наравне с прочими.

По имени, которое ключ одалживает, — а это и есть порядок, в котором перечисляет store: запись лежит на один уровень ниже карты, а ключи store упорядочивает по уровням.

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"]);

Число, записанное текстом, и упорядочено как текст, поэтому "10" раньше "9" — и Id<u16> идёт так же, по своему написанию, а не по числу. Имя с разделителем внутри упорядочено по имени: a.b встаёт перед a1b, потому что . ниже 1. Экранирование, которым store пишет этот разделитель, — это написание, а по написанию пути не сравнивают.

Порядок вставки нигде не записан. Если порядок важен — колонки таблицы, шаги в списке, — держите его сами, а карту берите для поиска.

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 срабатывает на любое изменение любого ключа, subscribe_key сужает до одного. Оба отдают guard, и подписка кончается, когда его дропают.

subscription_with() — то же самое, но со звеньями: .key(..) сужает, .external() пропускает собственные записи, .stream() забирает изменения в ваш собственный цикл вместо колбэка. См. Подписки.

Что такое external и по каким идентификаторам он различает записи — тема Подписок. Здесь о том, чем от них отличается карта.

На карте он фильтрует один Update. Insert, Remove и Clear доходят до каждого подписчика, включая того, кто их и вызвал.

Граница здесь между «правят значение» и «меняется то, что карта держит». Значение, которое вы записали сами, — ваше личное дело. А ключ, который появился или исчез, меняет форму карты, и виду, перечисляющему ключи, перестраиваться придётся — сам он этот ключ добавил или кто-то другой.

Отсюда следствие, которое стоит держать в голове: insert по новому ключу — это Insert, а по существующему — Update. Значит, вернётся ли к вам ваш собственный вызов, зависит от того, был ключ там или нет.

entry_cell(key) даёт ReactiveCell на одну запись — чтобы отдать одно значение туда, где про карту знать не полагается.