zena:signals
import {…} from 'zena:signals';
Signals: values that track which computations read them, and bring those computations up to date when they change.
The semantics follow the TC39 signals proposal
(https://github.com/tc39/proposal-signals): State is a value you set,
Computed is a cached function of other signals that runs only when
read, and Watcher is told synchronously when something it watches
might have changed. On top of that this module adds Effect, a
function that re-runs after the signals it read change, and Tracker,
a signal that holds no value, for classes that keep a reactive value
in an ordinary field. The design, and where this differs from the
proposal, is in docs/design/signals.md.
A write is pushed and then pulled. set walks the subscribers of the
state and marks each one stale, without running anything. The work
happens when something reads: a stale computed checks whether any of
its sources really changed, in the order it read them, and runs its
function only if one did.
Allocation: each signal is one object, and each dependency edge is one
Link. A computed that reads the same signals in the same order as
last time reuses its links, so re-running it allocates nothing. set,
the stale marking, and the queues of watchers and effects waiting to
run allocate nothing: the queues are linked through fields on the
nodes themselves.
Classes
SignalErrors
class SignalErrors extends Error
Several errors from callbacks that all had to run: the notify callbacks reached by one write, or the effects run after one, in the order they ran.
ReactiveNode
abstract class ReactiveNode
The common base of every signal, computed, effect and watcher: what
introspectSources and introspectSinks return, and what
Watcher.watch accepts.
new(kind: i32)
State
class State<T> extends ReactiveNode implements Signal<T>
A value that can be read and set. Reading it inside a computed or an
effect records the dependency; setting it to a value that is not
equals to the current one marks those consumers stale.
Computed
class Computed<T> extends Consumer implements Signal<T>
A value computed from other signals, and cached.
The function runs the first time the computed is read, and again only when it is read after one of the signals it read last time has changed. If the function throws, the error is cached the same way and thrown to every reader until a source changes.
Instead of passing a function, a subclass can override compute, which
saves allocating a closure per computed.
Watcher
class Watcher extends Consumer
Watches signals and is told, synchronously, when one of them might have
changed: the notify callback runs inside the set that caused it, after
the stale marking is done.
The callback may not read or write signals; it is for scheduling work
that reads them later. After notifying once, a watcher stays quiet
until watch is called again, with or without a signal.
Instead of passing a callback, a subclass can override notify.
new(callback: (w: Watcher) => void | null = null)
notify(): void
Called when a watched signal might have changed. Calls the callback given to the constructor unless a subclass overrides it.
watch(signal: ReactiveNode | null = null): void
Starts watching signal, which must be a State, Computed or
Tracker, and re-arms the watcher so the next change notifies it.
Call it with no argument just to re-arm.
unwatch(signal: ReactiveNode): void
Stops watching signal. Throws if it is not being watched.
getPending(): GrowableArray<ReactiveNode>
The watched computed signals that are stale, in the order they were watched. Reading them brings them up to date.
Effect
class Effect extends Consumer
A function that runs now, and runs again after any signal it read changes.
Effects do not run inside set. They run when the outermost write,
batch or computed read finishes, so a function that sets several
signals in a batch runs each affected effect once. An effect that was
marked only because a computed it reads might have changed checks that
computed first, and does not run if its value is the same.
An effect created while another effect runs belongs to it, and is
disposed when that effect runs again or is disposed. Functions passed
to onCleanup while the effect runs are called before its next run and
when it is disposed.
If the first run throws, the effect is disposed and the error is thrown from the constructor. An error from a later run is thrown from the operation that ran it, and the effect stays subscribed.
new(fn: () => void | null = null)
execute(): void
The effect's body. Calls the function given to the constructor unless a subclass overrides it.
The constructor runs the effect, so a subclass that overrides this
must set the fields it reads in its initializer list, which runs
before super(). A constructor body runs too late.
dispose(): void
Stops the effect: runs its cleanups, disposes the effects it owns, and unsubscribes it from everything it read.
Tracker
final class Tracker extends ReactiveNode
A signal with no value of its own: the dependency-tracking part of a
State, for a value stored somewhere else.
A class that keeps a reactive value in an ordinary field calls
reportRead in the field's getter and reportChanged in its setter
when the value changes. The value is then read straight from the
field. A tracker is only needed once something reactive reads the
field, so a class can create it on the first read inside a computed or
effect (isTracking() tells it when) and leave it null until then.
See docs/design/signals.md.
Interfaces
Signal
Type aliases
SignalOptions
type SignalOptions<T> = {equals?: (a: T, b: T) => boolean, watched?: () => void, unwatched?: () => void}
Options for State and Computed.
equals decides whether a new value is the same as the old one; when
it is, consumers are not told about it. The default is ==.
watched is called when the signal gains its first live consumer (an
effect, a watcher, or a computed something live reads), and
unwatched when it loses the last one. Neither may read or write
signals. They run once the operation that caused them finishes.
TrackerOptions
type TrackerOptions = {watched?: () => void, unwatched?: () => void}
Options for Tracker: callbacks for when it gains its first live
consumer and loses its last one. See SignalOptions.
Functions
effect
function effect(fn: () => void): Effect
Creates an effect that runs fn now and again whenever a signal it
read changes. Shorthand for new Effect(fn).
onCleanup
function onCleanup(fn: () => void): void
Registers fn to be called before the running effect runs again, and
when it is disposed. Throws if no effect is running.
batch
function batch<R>(fn: () => R): R
Runs fn, and runs the effects it caused once it returns, so an effect
that depends on several signals fn sets runs once. Watchers are still
notified inside each set. Batches nest; effects wait for the
outermost.
untrack
function untrack<R>(fn: () => R): R
Runs fn without recording what it reads against the running computed
or effect. Unsafe in the TC39 sense: a computed that reads a signal
this way is not updated when that signal changes.
isTracking
function isTracking(): boolean
Whether a computed or effect is running and recording what it reads.
currentComputed
function currentComputed(): ReactiveNode | null
The computed or effect that is recording reads, if any.
introspectSources
function introspectSources(node: ReactiveNode): GrowableArray<ReactiveNode>
The signals node read on its last run, in the order it first read
them, or the signals a watcher watches. Empty for a State or
Tracker.
introspectSinks
function introspectSinks(node: ReactiveNode): GrowableArray<ReactiveNode>
The live consumers that read node: watchers watching it, effects that
read it, and computeds that read it and are themselves live. A computed
that nothing live reads does not appear.
hasSinks
function hasSinks(node: ReactiveNode): boolean
Whether node has a live consumer.
hasSources
function hasSources(node: ReactiveNode): boolean
Whether node read any signal on its last run, or watches any.