invalidation
invalidation provides dependency-aware invalidation primitives for
incremental systems.
It is a small #![no_std] crate for situations where upstream changes must
flow through a dependency graph and downstream work should be processed in a
clear, explicit order.
The crate is centered on a small set of building blocks:
ChannelandChannelSetfor named invalidation domainsInvalidationGraphfor dependency edges and cycle handlingInvalidationSetfor accumulated invalidated keysEagerPolicyandLazyPolicyfor propagation strategyInvalidationTrackerfor coordinated graph, set, cascade, and cross-channel workflowsDrainBuilderand deterministic drain helpers for ordered processing
It intentionally does not own recomputation, caching, or scheduling. Those remain in your application.
Quick Start
use ;
const LAYOUT: Channel = new;
let mut tracker = new;
tracker.add_dependency.unwrap;
tracker.add_dependency.unwrap;
tracker.mark_with;
let ordered: = tracker.drain_sorted.collect;
assert_eq!;
Concepts
- key: a node identifier in your system
- channel: an invalidation domain such as layout or paint
- dependency:
A depends on BmeansBmust be processed beforeA - invalidated root: a key you explicitly mark invalidated
- affected key: an invalidated root or one of its transitive dependents
- drain: consume invalidated work in dependency order and clear it
Choosing The Main API
- Use
InvalidationTrackerwhen one coordinator should own dependency edges, invalidation state, channel cascades, and cross-channel edges. - Use
InvalidationGraphplusInvalidationSetseparately if your embedder already owns propagation or scheduling policy and only wants the primitives. - Use
DrainBuilderwhen you need deterministic ordering, targeted drains, scratch reuse, or tracing. - Use
intern::Internerwhen your natural keys are strings or other non-Copyvalues.
Eager vs Lazy
Two workflows are intentionally first-class:
EagerPolicy: propagate immediately at mark time, then usedrain_sorted.LazyPolicy: mark only roots at change time, then expand withdrain_affected_sortedorDrainBuilder::affected.
use ;
const LAYOUT: Channel = new;
let mut eager = new;
eager.add_dependency.unwrap;
eager.add_dependency.unwrap;
eager.mark_with;
assert!;
let mut lazy = new;
lazy.add_dependency.unwrap;
lazy.add_dependency.unwrap;
lazy.mark_with;
assert!;
let ordered: = lazy.drain_affected_sorted.collect;
assert_eq!;
Marking Work
Use mark_with for normal update workflows. It applies the chosen propagation
policy, same-key channel cascades, and cross-channel edges until the
invalidation closure is complete.
Use mark only when you intentionally want a direct mark without same-channel
graph traversal or cross-channel edge traversal. It still applies same-key
channel cascades.
Cascades And Cross-Channel Edges
Use the tracker for the common case. ChannelCascade and CrossChannelEdges
are the standalone primitives; InvalidationTracker owns them and applies them
as part of marking. See the runnable cascade_cross_channel example for the
same workflow in a complete program, and retained_pipeline for a node-scoped
multi-phase example.
use ;
const LAYOUT: Channel = new;
const PAINT: Channel = new;
let mut tracker = new;
// Same key, different channel: invalidating layout also invalidates paint.
tracker.add_cascade.unwrap;
// Different key and channel: node 1 layout feeds node 2 paint.
tracker.add_cross_dependency;
tracker.mark_with;
assert!;
assert!;
assert!;
Deterministic And Targeted Drains
When ties must be stable, or you only want to process part of the invalidated
region, use DrainBuilder:
use ;
const LAYOUT: Channel = new;
let mut tracker = new;
tracker.add_dependency.unwrap;
tracker.add_dependency.unwrap;
tracker.add_dependency.unwrap;
tracker.add_dependency.unwrap;
tracker.mark;
tracker.mark;
tracker.mark;
tracker.mark;
tracker.mark;
let focused: = tracker
.drain
.within_dependencies_of
.deterministic
.run
.collect;
assert_eq!;
assert!;
assert!;
Non-Copy Keys
invalidation keeps its core APIs keyed by K: Copy so hot paths stay
predictable. If your natural keys are strings or other owned values, intern
them first:
use ;
const STYLE: Channel = new;
;
let mut ids = new;
let stylesheet = ids.intern;
let button = ids.intern;
let mut tracker = new;
tracker.add_dependency.unwrap;
tracker.mark_with;
let affected: = tracker.drain_affected_sorted.collect;
assert_eq!;
Gotchas
add_dependency(a, b, ...)meansadepends onb, not the reverse.markdoes not follow graph dependents or cross-channel edges; usemark_withfor the usual full-closure update path.LazyPolicyanddrain_sortedare usually the wrong pair.- Deterministic dense drains assume a compact key space; use
Internerwhen keys are sparse or structured. - If cycles are allowed, topological drains can stall.
Minimum Supported Rust Version (MSRV)
This version of invalidation has been verified to compile with Rust
1.88 and later.
Future versions might increase the Rust version requirement. That is not treated as a breaking change and may happen in minor or patch releases.