firsthand

ADR-0011: Eager unlinking, and leak tests that can actually fail

Status: accepted (Phase 1)

Problem

A long-lived application creates and destroys millions of reactive scopes. Any retained reference — a subscriber link, a context record, an event handler, a DOM node — accumulates. “No leaks” must be a tested property, not an intention.

Constraints

Options considered

  1. Rely on GC and weak collections throughout (WeakRef subscribers, FinalizationRegistry cleanup). Removes whole classes of leaks, but makes update semantics non-deterministic (a subscriber may vanish mid-flush) and costs weak-ref overhead on the hottest structure in the framework.
  2. Eager, explicit unlinking on disposal, with strong references in the graph. Deterministic, fast, but every path that creates a subscription must have a matching disposal path — which is exactly what the owner tree guarantees structurally.
  3. No explicit disposal, detach on next notify. Cheap to implement, leaks until something notifies, unbounded in the general case.

Chosen design

Option 2. Every subscription is created under an owner; owner disposal unlinks every edge in both directions, runs cleanups, removes delegated handler entries and detaches DOM. WeakRef appears only in tests, never in the runtime hot path.

Testing strategy

Performance implications

Strong references and pointer unlinking are the fast option; weak collections would add indirection to every subscriber walk. Disposal is O(edges in the subtree) with no allocation.

Memory implications

The point of the ADR: retained size after disposal returns to the pre-creation baseline modulo allocator noise, and that is asserted rather than assumed.

DX implications

Developers rarely dispose manually — owners do it. The exception is code created outside a component, where createRoot hands back an explicit dispose and the documentation says plainly that not calling it leaks.

Rejected alternatives

Weak-everything (non-deterministic updates, hot-path cost), detach-on-notify (unbounded retention), and “assume GC handles it” (untestable, and wrong as soon as one global subscriber list exists).