Skip to main content

Crate day_model

Crate day_model 

Source
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>: a Copy, process-lifetime handle to one value, like Signal::global.
  • Keyed<T>: a collection addressed by stable key, with its own key→index map.
  • Elem<T>: one element of a keyed store; Elem::exists is the tracked deletion guard.
  • Field: one field projected out of any Source; Copy, itself a Source (so fields nest to any depth), and a Binding, so every control binds to it directly.
  • #[derive(Observable)] (in day-macros) generates the typed field accessors, so store.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.
ChangeSinkId
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 a Source (so fields nest), and a Binding (so every control binds to it).
FieldSession
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, #RRGGBB as 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 M row: 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::key recovers 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.
UndoCheckpoint
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.
UndoGroup
RAII lifetime of an undo group; dropping the last nested guard seals its changes.
UndoStack
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 Keyed slot, 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§

ApplyField
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 above 1 << 63 is 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_changes is scoped to one closure, a sink lives until removed; it is how a persistence container watches the stores it loaded. Sinks receive the same Change the 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 project rather than field so a struct may have a field called field without 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 inside f: 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 on holders exist: the undo stack’s standing form of what record_values does for one closure.
with_author
Run f with changes stamped as author: 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.