Runtime Architecture / TypeScript
TypeScript Reactivity Engine
A zero-dependency reactive runtime built from first principles to study signals, effects, batching, lazy computed values, ownership, and cycle detection.
- Context
- Independent / Open Source
- Source Code
- Public source
Engineering scope
Engineering Scope
Stack: TypeScript · Signals · Computed values · Effects · Linked signals
Inspect the source ↗I built this zero-dependency TypeScript runtime to study how signals, computed values, and effects work beneath a framework. Four implementations develop dependency cleanup, lazy evaluation, batching, ownership, and cycle detection, with source code available for each stage.
An experimental runtime with runnable examples. Scheduling, error recovery and nested disposal still need broader automated coverage.
01 / Dependencies
Subscriptions must follow the current branch
A reactive computation can read a different set of signals after a condition changes. Keeping every previous subscription causes obsolete branches to continue triggering work.
I gave each reactive node a set of sources and observers. Before reevaluation, cleanupNode removes its previous subscriptions; reads under activeContext then collect the dependencies used by the new execution. I chose to rebuild that set so inactive branches stop receiving updates.
Rebuilding dependencies adds bookkeeping to each evaluation. It makes dynamic branches inspectable, but cleanup and dependency collection must remain correct when user code throws.
02 / Scheduling
Separate invalidation, evaluation and effect scheduling
Immediate propagation through a diamond-shaped graph can expose partially updated derived values. Multiple writes belonging to one operation can also trigger repeated observations.
I introduced CLEAN, CHECK and DIRTY states and made computed values evaluate on demand. For grouped writes, I track batch nesting and collect pending effects in a Set, flushing them when the outermost batch finishes.
Batching reduces repeated effect runs, but makes execution order more complicated. Cached values and queued effects have to stay consistent when computations throw or trigger further writes.
03 / Lifecycle
Detect feedback loops and dispose of subscriptions
A computation that reads and writes the same dependency can reenter itself. Long-lived subscriptions also need a way to stop reacting when the operation that created them ends.
I added an execution stack to detect reentry and throw a cycle error. Effects return a disposer, and createRoot establishes an ownership context with cleanup callbacks. I used separate examples to explore cycles, disposal and custom equality.
Nested ownership remains experimental. The root and effect cleanup paths need broader regression coverage before they can be relied on to dispose of every nested subscription.
04 / Equality
Choose when a value counts as changed
An array or object can be replaced by an equivalent value. Notifying every consumer would repeat work even when the application considers the value unchanged.
I made equality configurable for signals and computed values, using Object.is by default. I also added linkedSignal to explore a value that can be overridden locally and recomputed when its source changes.
Custom comparison adds work to each update. If it ignores a field that matters to a consumer, it can suppress a necessary notification; deep comparison is useful only when that cost and behavior fit the data.
System flow
From write to observation
- 01
Read
An active computation reads signals and establishes its dependencies.
- 02
Invalidate
A write marks dependent work as needing another evaluation.
- 03
Coordinate
Batching and lazy evaluation coordinate when dependent work becomes current.
- 04
Observe
Effects consume updated values; ownership gives that work a cleanup boundary.
Evidence & further reading