///|
/// User-facing push-mode eager derived cell.
///
/// Recomputed eagerly when any upstream cell changes. Value is cached;
/// consecutive reads without upstream changes do not trigger recomputation.
pub(all) struct EagerDerived[T] {
priv cell_id : @incr_types.ReactiveId[T]
priv value : Ref[T?]
priv rt : Runtime
}
///|
/// Creates an eager derived value.
///
/// `compute_fn` is invoked immediately to set the initial value and establish
/// dependencies. Whenever any dependency changes, the runtime calls
/// `compute_fn` again via push propagation (triggered by `Input::set` or
/// `Runtime::batch`).
///
/// # Type Erasure
///
/// `compute_fn` is captured in a `() -> Bool` closure stored on
/// `PushReactiveData`. The Bool indicates whether the value changed
/// (enabling early cutoff for downstream push cells).
pub fn[T : Eq] EagerDerived::EagerDerived(
rt : Runtime,
compute_fn : () -> T,
) -> EagerDerived[T] {
let value : Ref[T?] = { val: None }
// Type-erased compute: invoked by push_propagate_from after begin_tracking.
let compute : () -> Bool = () => {
let new_val = compute_fn()
let changed = match value.val {
None => true
Some(v) => v != new_val
}
value.val = Some(new_val)
changed
}
let (cell_id, reactive_idx) = rt.install_cell(
rt.push.free_reactives,
rt.push.reactives,
idx => PushReactive(idx),
cell_id => {
meta: {
cell_id,
label: None,
changed_at: Revision::initial(),
durability: Low,
subscribers: @hashset.HashSet([]),
push_reachable_count: 0,
},
compute,
sources: [],
level: 1,
dirty: false,
},
)
rt.push.node_count = rt.push.node_count + 1
let derived : EagerDerived[T] = {
cell_id: @incr_types.ReactiveId::{ id: cell_id },
value,
rt,
}
// Initial compute: establish dependencies and set initial value.
rt.begin_tracking(cell_id)
let _ = compute()
let (new_sources, new_seen) = rt.end_tracking()
rt.push.reactives[reactive_idx].sources = new_sources
rt.push.reactives[reactive_idx].level = rt.recompute_level(
cell_id, new_sources,
)
rt.finish_tracking(cell_id, [], new_sources, new_seen)
if rt.has_pending_events() {
rt.drain_pending_events_if_idle()
}
derived
}
///|
/// Returns the current cached value of the eager derived cell.
///
/// Must be called inside a tracked context (a memo or reactive compute function).
/// Outside a compute function, use `EagerDerived::read()` or
/// `EagerDerived::watch()`.
pub fn[T] EagerDerived::get(self : EagerDerived[T]) -> T {
guard self.rt.core.tracking.stack.length() > 0 else {
abort(
"EagerDerived::get() called outside tracked context. Use EagerDerived::read() or EagerDerived::watch() to read from outside the graph.",
)
}
self.read_permissive()
}
///|
fn[T] EagerDerived::read_permissive(self : EagerDerived[T]) -> T {
self.rt.check_cross_runtime(self.rt.core.runtime_id, "EagerDerived")
let cell_id = self.cell_id.id
guard !self.rt.is_cell_disposed(cell_id) else {
abort("EagerDerived::read called on a disposed eager derived cell")
}
Tracker::record_dependency(self.rt, cell_id)
self.value.val.unwrap()
}
///|
/// Permissive read. Works outside the graph and records a dependency if tracked.
pub fn[T] EagerDerived::read(self : EagerDerived[T]) -> T {
self.read_permissive()
}
///|
/// Returns the unique cell identifier for this eager derived value. Stable
/// across reads — useful for graph-shape probes (gc anchoring, edge
/// inspection) where a cell needs an identity independent of its value.
pub fn[T] EagerDerived::id(self : EagerDerived[T]) -> CellId {
self.cell_id.id
}
///|
/// Returns true if this eager derived cell has been disposed.
pub fn[T] EagerDerived::is_disposed(self : EagerDerived[T]) -> Bool {
self.rt.is_cell_disposed(self.cell_id.id)
}
///|
/// Removes this eager derived cell from the dependency graph.
///
/// After disposal:
/// - The cell is removed from all source subscriber sets.
/// - `cell_index[id]` is set to `Disposed`.
/// - The SoA slot is added to `free_push_reactives` for future reuse.
pub fn[T] EagerDerived::dispose(self : EagerDerived[T]) -> Unit {
self.rt.dispose_cell(self.cell_id.id)
}