src — reactivity
tiny-test.mjs test runner (plumbing)
spec.mjs executable specification — run this
reactivity.js ← YOU WRITE THIS (mini-vue)
signals.js ← YOU WRITE THIS TOO, independently
node spec.mjs
Required exports from reactivity.js
reactive(obj) ref(value) effect(fn) computed(fn) watch(source, cb) batch(fn)
effect(fn) runs fn immediately. batch(fn) flushes queued effects once at the end.
The two tests that matter
Stage 3 — an effect that never read b must not re-run when b changes. If that fails you
have built "re-run everything on any write," which is not dependency tracking. Everything else in
this module is downstream of getting stage 3 right.
Stage 5 — the stale-dependency test. An effect reads a or b depending on a flag; after the
flag flips, writing the no-longer-read property must not trigger it. Failing here is a
dependency leak: effects accumulate dependencies forever and fire on unrelated writes. This is
precisely the bug that cleanup-before-re-run exists to prevent, and it is much more convincing
after you have watched it happen.
Stage 6 fails if you use a single global "current effect" instead of a stack — the inner effect steals the outer's dependencies.
Then build signals independently
Write signals.js from scratch with a minimal API (signal, computed, effect, batch).
Do not refactor the Vue code into it. The point is discovering how much of reactivity.js
was essential and how much was Vue-specific. Copy this spec to signals.spec.mjs and adapt the
API surface; the behavioural requirements should be identical, which is itself the finding.