Expand description
Per-property observation: a store whose writes wake only the readers of the field that changed.
With one Signal<Vec<Item>>, every observer of the store re-runs when any field of any
element changes. Day’s bind equality gate keeps the native side precise, so what is wasted
is compute: every row’s closures re-run and re-clone on every keystroke, and the waste grows
with the list. Here each (element, field) is its own dependency node, so a write wakes exactly
the readers of that field, plus readers who asked for something coarser by reading something
coarser.
The pieces:
Store<T>: aCopy, process-lifetime handle to one value, likeSignal::global.Keyed<T>: a collection addressed by stable key, with its own key→index map.Elem<T>: one element of a keyed store;Elem::existsis the tracked deletion guard.Field: one field projected out of anySource;Copy, itself aSource(so fields nest to any depth), and aBinding, so every control binds to it directly.#[derive(Observable)](in day-macros) generates the typed field accessors, sostore.elem(id).name()is written once per struct, not once per call site.- The change log: every write announces
(path components, field label, operation), with the slot’s prior and new value captured when a consumer asks (record_values). What the tests read today; the persistence layer’s input later.
§Paths
A path names one observable slot: a store, one element of it, one field, a field of that
field, to any depth. Each gets its own Trigger (day-reactive’s data-less dependency node),
created lazily on first observation: a path nobody reads has no trigger and costs nothing to
write. Reads track the most specific path they touched; writes notify that path and its
ancestors. That asymmetry is the entire granularity story.
A path is a parent (an interned id) plus one component. The leaf component is not interned, so building a field handle (the thing that happens on every read of every row) costs no lookup. Interning happens only where a path is used as a parent: once per store, once per element handle, once per nested struct.
§Reclamation
Both tables shrink. Triggers are refcounted by the scopes observing them: a binding’s scope
dies (a page popped, a row recycled) and the last watcher out disposes the trigger. Interner
entries are refcounted by their children and their triggers: when the last trigger under an
element is released, the element’s entry (and any empty ancestors up to its pinned store root)
is freed. A freed slot’s id carries a generation, and every handle validates its cached id
before use, re-interning through its own handle chain when stale, so a Copy handle held
across a reclamation heals itself, and everyone converges on the current identity.
A claim made from inside a reactive computation belongs to that computation’s current run,
not to a scope: it is released when the computation re-tracks or dies
(day_reactive::on_run_retrack), exactly mirroring day-reactive’s own per-run source
bookkeeping. This is what keeps a long-lived recycled list cell from accumulating claims for
every row it ever showed: rotate a binding across a million rows and the claim count stays
at one row’s worth. Outside any computation (a build seeding an initial value, an event
handler) a tracked read subscribes nothing and therefore claims nothing: no trigger is
created at all, because nothing could ever wake through it.
§Threads
Store is Send + Sync (when T is); the trigger tables are thread-local to the main
thread. A worker edits through Store::transact, naming what it touched as plain path
components; dropping the transaction commits the data and queues the announcements, and
Store::pump, called on the main thread, wakes exactly the paths the worker named. (The
persistence container will schedule the pump itself; a bare store keeps delivery explicit.)
Re-exports§
pub use uuid::Uuid;
Structs§
- Change
- One announced change, in the form a store outside the UI can consume.
- Change
Sink Id - Handle for one installed change sink; pass it back to
remove_change_sink. - Elem
- One element of a keyed store, addressed by a key that survives reordering.
- Field
- One field, projected out of any
Source.Copy, itself aSource(so fields nest), and aBinding(so every control binds to it). - Field
Session - An explicit preview session over one field (see
Field::session).Copy, like the field itself. - Keyed
- A keyed collection that keeps its own key→index map, so an element read is O(1) rather than a scan, and no caller can forget to maintain the map, because the collection does it.
- Mapped
- A converted view of a field: an ISO string as a date,
#RRGGBBas a color, an index into a table as the value it names.Copy, and still a binding, so it goes straight into a control. - ModelId
- A typed, opaque id for one
Mrow:Copy, 8 bytes, cheap to pass and compare. It wraps the key’s path handle:Store::elem, query results, list slots and destinations all speak it, and a wrong-model id is a compile error rather than a wrong row.ModelId::keyrecovers the real key for display, deep links, or the persistence edge. - NodeId
- An interned interior node: a path that something hangs off. Carries a generation so a handle can tell a live id from one whose slot was reclaimed and reused.
- Numeric
usize⇄f64.- Path
- One observable slot: a parent, and one step down from it.
- Store
- A
Copy, process-lifetime handle to one observable value. - Tx
- An open background transaction. Name what you touch with
Field::touch(or push components directly); commit by dropping. - Undo
Checkpoint - Queue-confined history checkpoint for a persistence transaction. Restore it only after rolling back the corresponding model changes. It does not snapshot the models themselves.
- Undo
Group - RAII lifetime of an undo group; dropping the last nested guard seals its changes.
- Undo
Stack - One undo history over the stores it
UndoStack::watches: units are turns (everything one event’s dispatch changed), inverted, not snapshots. Enabling it turns on standing value capture; drop the stack and the capture stops.
Enums§
- Key
- One element’s key: what its paths, its
Keyedslot, and (under persistence) its stored row are addressed by. - Op
- What kind of change was announced. A persistence layer needs this to choose between an INSERT, an UPDATE and a DELETE; the UI does not care, which is why it rides alongside the path rather than inside it.
Constants§
- STRUCTURE
- The reserved component naming a collection’s shape (which keys, in what order) as opposed to any element’s contents.
Traits§
- Apply
Field - Write one captured field value back into a struct, by label. Implemented by
#[derive(Observable)]; this trait is how a type-erased undo unit re-enters the store it came from. - AsKey
- A value usable as a key: what an
#[obs(key)]/#[model(id)]field’s type implements. Integer keys reserve the top bit (the interned-handle space); a negative cast or a hash-derived id at or above1 << 63is refused in debug builds rather than silently colliding with an interned handle. - Identified
- Gives an element the stable key its paths are addressed by. Implemented by
#[derive(Observable)]from the#[obs(key)]field. - Source
- Anything a field can be projected out of: the store itself, one element of a keyed collection, or another field (which is how nesting works).
Functions§
- announce
- Announce a change named by plain components: how a background transaction’s writes reach
the triggers on the main thread, and what a framework layer calls to wake a path it
maintains for a value that lives elsewhere (a relation index announcing a parent’s to-many
field). Wakes the deepest path whose interior steps are interned; anything deeper cannot
have an observer, because observing is what interns. The change feeds sinks and the
recorder with the current author tag and op
Set, carrying no values. - field_
id - A field’s path component, derived from its name at compile time.
- install_
change_ sink - Install a standing consumer of every announced change (main thread). Where
record_changesis scoped to one closure, a sink lives until removed; it is how a persistence container watches the stores it loaded. Sinks receive the sameChangethe recorder would. A sink may write stores re-entrantly. The pipeline nests: a write made from inside a sink completes its own announcement (sinks included) before the outer one resumes, which is how relation maintenance cascades a delete. But a sink that writes owns its own termination: nothing here bounds the recursion. - interned_
keys - How many wide keys the process has interned: the cost of wide-key identity itself, assertable in a test. Entries are retained for the process’s lifetime.
- interned_
nodes - How many interior nodes are currently interned (the sentinel excluded): the cost of observation’s second table, assertable in a test.
- observed_
paths - How many paths currently have a trigger: the cost of observation, assertable in a test.
- project
- What the derive calls. Named
projectrather thanfieldso a struct may have a field calledfieldwithout a collision. - record
- Just the labels, for the tests that only care which fields moved.
- record_
capture_ active - Whether any change consumer currently captures prior/new values: a scoped
record_values, or a standing holder like an installed undo stack. A framework layer (the persistence container) asks this to decide whether a delete must materialize its row first: a value-free delete cannot be inverted, and capturing costs nothing when nobody records. - record_
changes - Record every change announced inside
f. What the tests read, and the persistence layer’s input. - record_
values record_changes, with prior/new values captured on every field write insidef: the form an undo unit needs. Costs one clone per write while active, nothing when not.- remove_
change_ sink - want_
values_ standing - Keep prior/new values flowing on every change while
onholders exist: the undo stack’s standing form of whatrecord_valuesdoes for one closure. - with_
author - Run
fwith changes stamped asauthor: how an undo replay or an importer signs its writes so consumers (a sync engine, a query) can tell them from the user’s.
Type Aliases§
- TxPaths
- What a transaction touched, in a form that survives the trip to the main thread: the path’s components, outermost first, plus the label the change log records.