///|
fn Runtime::add_read_root(self : Runtime, id : CellId) -> Unit {
  let prev = self.add_gc_root(id)
  if prev == 0 {
    self.cell_lifecycle[id.id].on_observe(self, id)
  }
}

///|
fn Runtime::remove_read_root(self : Runtime, id : CellId) -> Unit {
  if self.is_cell_disposed(id) {
    self.core.gc_root_counts.remove(id)
    return
  }
  let remaining = self.remove_gc_root(id)
  if remaining == 0 {
    self.cell_lifecycle[id.id].on_unobserve(self, id)
  }
}

///|
/// A typed persistent read root for values read outside the graph.
///
/// A `Watch` keeps its target reachable across `Runtime::gc()` and preserves
/// read errors as values.
pub struct Watch[T] {
  priv runtime : Runtime
  priv target_id : CellId
  priv getter : () -> Result[T, ReadError]
  priv mut disposed : Bool
} derive(Debug(ignore=[Runtime, Fn, CellId]))

///|
/// Returns the current value or a mechanism error (cycle / disposed) from the
/// watched cell.
pub fn[T] Watch::read(self : Watch[T]) -> Result[T, ReadError] {
  guard !self.disposed else { abort("Watch: already disposed") }
  let value = (self.getter)()
  if self.runtime.has_pending_events() {
    self.runtime.drain_pending_events_if_idle()
  }
  value
}

///|
/// Returns the current value, aborting if a read error is detected.
pub fn[T] Watch::read_or_abort(self : Watch[T]) -> T {
  match self.read() {
    Ok(value) => value
    Err(e) => abort(e.format_path())
  }
}

///|
/// Releases this watch's keep-alive hold on the target cell.
///
/// Idempotent: disposing an already-disposed watch is a no-op.
pub fn[T] Watch::dispose(self : Watch[T]) -> Unit {
  guard !self.disposed else { return }
  self.disposed = true
  self.runtime.remove_read_root(self.target_id)
}

///|
/// Returns true if this watch has been disposed.
pub fn[T] Watch::is_disposed(self : Watch[T]) -> Bool {
  self.disposed
}

///|
fn[T] EagerDerived::watch_result(self : EagerDerived[T]) -> Watch[T] {
  let cell_id = self.cell_id.id
  guard !self.rt.is_cell_disposed(cell_id) else {
    abort("EagerDerived::watch called on a disposed eager derived cell")
  }
  let rt = self.rt
  rt.add_read_root(cell_id)
  {
    runtime: rt,
    target_id: cell_id,
    // EagerDerived has no cycle/raise channel (its compute is `() -> T`), but
    // the `Watch` contract is uniformly honest about disposal: a read after the
    // target is disposed returns `Err(Disposed(_))` rather than aborting.
    getter: fn() {
      if self.rt.is_cell_disposed(cell_id) {
        Err(ReadError::disposed(cell_id))
      } else {
        Ok(self.read_permissive())
      }
    },
    disposed: false,
  }
}