// Eq-based memo.
//
// `memo` cuts off on identity: it stops propagating when a recomputation
// returns the *same object* as last time (`physical_equal`). That is the
// right default for scalars and for values meant to be shared by reference,
// but it means a memo that maps many different inputs onto an equal-but-
// freshly-allocated value wakes every dependent on every source change.
// `memo_eq` compares with `Eq` instead.

///|
/// A memo whose cutoff compares values with `Eq` rather than object identity.
///
/// Reach for this when `compute` returns a freshly allocated value that is
/// often equal to the previous one — a formatted `String`, a small struct, a
/// bucketed number. `memo` republishes each of those because every result is
/// a new object; `memo_eq` republishes only when the value actually moved.
///
/// ```mbt check
/// test "memo_eq stops an effect that memo would wake" {
///   let n = @resource.signal(0)
///   // A fresh String every time, but the same text until `n` reaches 100.
///   let label = @resource.memo_eq(() => "page \{n.get() / 100}")
///   let runs = Ref(0)
///   let _ = @resource.render_effect(() => {
///     let _ = label()
///     runs.val += 1
///   })
///   for i in 1..=5 {
///     n.set(i)
///   }
///   // Five source changes, one effect run: the label never moved.
///   inspect(runs.val, content="1")
///   inspect(label(), content="page 0")
/// }
/// ```
///
/// Two differences from `memo` worth knowing:
///
/// - **It is eager.** `memo` is lazy — `compute` first runs on the first
///   read. `memo_eq` runs it once at construction and again on every source
///   change, whether or not anything reads the result. Per source change the
///   two cost the same single `compute` call.
/// - **It is owned.** It installs an effect, registered with the current
///   owner, so it stops recomputing once that owner is disposed; a disposed
///   `memo_eq` keeps returning the last value it published. Created outside
///   any owner it lives as long as the program, like a bare `render_effect`.
///
/// Reads are always current: a read straight after `set`, with no flush in
/// between, sees the new value.
pub fn[T : Eq] memo_eq(compute : () -> T) -> () -> T {
  // The inner memo owns the caching and the recomputation; `out` republishes
  // its value only when `Eq` says it moved, and that is the cutoff dependents
  // observe. Seeding `out` through `untracked` does two jobs: it keeps an
  // enclosing effect from picking up `compute`'s dependencies as its own, and
  // it leaves `inner` warm so the watch below reuses the cached value instead
  // of running `compute` a second time.
  let inner = @signals.memo(compute)
  let out = @signals.signal(@signals.untracked(inner))
  let _ = @signals.watch(inner, (next, _prev) => out.set(next))
  () => out.get()
}