Producers and the tick
Who may write which field, how writes reach the store, and what a commit tells you.
Views read the store. Producers write it. A producer is your code, holding a licence to set named fields of one scope. Writes only happen inside Store::tick, and each tick ends in one commit.
Claiming fields
let editor = store.producer::<Editor>(EDITOR, &["title", "dirty", "words"])?;
Store::producer returns a Producer<T> for one scope and the fields it names. Every field has at most one writer, and that is checked here, at startup:
- A name the schema does not declare is
StoreError::NoSuchField. - A field another producer already claimed is
StoreError::AlreadyClaimed.
Two writers for one fact is a bug you cannot see at runtime, because each looks right on the frames it wins. Carte makes it fail before the first frame.
Several producers may write one scope, as long as they claim different fields. The notes example has a library producer for the note lists and a prefs producer for the theme, both on the process scope. Producer is not Clone, because a copy would be a second writer the startup check never saw.
Ticking
let notice = store.tick(&mut outbox, |actions, barrier| {
for a in actions {
model.apply(a);
}
if let Ok(mut w) = barrier.writer(&editor) {
set!(w, dirty, true).unwrap();
set!(w, words, 42).unwrap();
}
});
tick does three things, in this order:
- Drains the outbox and hands you the actions.
- Runs your closure with a
Barrier. Apply the actions to your model here, then run your producers. - Commits, and returns a
Notice.
barrier.writer(&producer) opens the producer's scope for writing. It fails with StoreError::NotMounted while the scope's node is off the path. That is normal, not an error to report: the editor's producer simply has nothing to write while no note is open. Run every producer every tick and let writer tell you which scopes exist.
Setting fields
set!(w, words, 42)?;
The set! macro expands to w.set("words", |scope| &mut scope.words, 42). The schema's field name and the struct's field come from one token, so they cannot drift apart. Call Writer::set directly only for a field name no Rust identifier can spell.
Every set is compared with the current value:
Ok(true): the value changed.Ok(false): it was already that value. Nothing happens and nobody wakes.Err(StoreError::Unclaimed { .. }): this producer did not claim the field.
So producers can write every field on every tick without tracking what changed. Dirtiness is worked out from values, never from flags you remember to set. If a producer changes a field and then puts it back within one tick, that is no change either.
Grain
A float that changes every frame defeats the equality check and wakes every subscriber every frame. Declare the precision consumers act on, and set with set_num!:
// in reflect():
Field::new("elapsed", Kind::Float).at_grain(1.0)
// in the tick:
set_num!(w, elapsed, clock.seconds())?;
set_num! floors the value to the declared grain before comparing, so a timer shown in whole seconds wakes its watchers once a second. Without a declared grain it behaves like set!. It works on any type that implements Grained: the built-in integer and float types. Writer::grain(name) returns a field's Grain if you would rather round where you compute the value.
Working and committed state
Inside the barrier there are two copies of every mounted scope. Producers write and read working state. Views only ever see committed state, and they cannot look while the barrier exists, because it borrows the store mutably.
A producer that derives one fact from others, such as a summary built from two scopes, reads working state so it sees this tick's writes:
let shared = barrier.working::<Shared>(Scope::Process);
Writer::working() returns the producer's own scope as written so far. Barrier::committed::<T>(scope) returns last tick's value, unchanged by anything in this tick. Derived facts are ordinary producers, run inside the same tick, so no view sees a half-updated derivation.
TODO: producer order within a tick is the order you call them. There is no declared dependency between producers yet.
The notice
A tick returns one Notice for everything it changed:
| Method | Returns |
|---|---|
woken() | Every subscriber owed a re-render, each once, in id order |
woke(id) | Whether this subscriber is owed one |
changed() | Every field that moved, as Changed { scope, field } |
is_empty() | Whether the tick changed anything |
if notice.woke(watcher) {
// re-render whatever reads `dirty`
}
Ten sets are one notice. A subscriber watching five fields that all moved appears once. A scope mounting or unmounting since the last tick wakes its subscribers in this notice too.
Facts, not presentation
Producers set the fact; views do the easing. If a panel slides open, the store holds open: true, and the view animates towards it. An eased value in the store would change every frame for no reason.
Next
Intents and the outbox: how views ask for things.