zena:signals

zena
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

zena
class SignalErrors extends Error
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.

Constructors
zena
new()
#
Properties
zena
errors: Array<Error>
#
2 inherited members
From Error
Properties
zena
message: String
#
Methods
zena
getStackTrace(): String | null
#

ReactiveNode

zena
abstract class ReactiveNode

The common base of every signal, computed, effect and watcher: what introspectSources and introspectSinks return, and what Watcher.watch accepts.

Constructors
zena
new(kind: i32)
#

State

zena
class State<T> extends ReactiveNode implements Signal<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.

Constructors
zena
new(initial: T, options: SignalOptions<T> | null = null)
#
Methods
zena
get(): T
#
zena
set(value: T): void
#

Computed

zena
class Computed<T> extends Consumer implements Signal<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.

Constructors
zena
new(fn: () => T | null = null, options: SignalOptions<T> | null = null)
#
Methods
zena
get(): T
#
zena
compute(): T
#

Computes the value. Calls the function given to the constructor unless a subclass overrides it.

Watcher

zena
class Watcher extends Consumer
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.

Constructors
zena
new(callback: (w: Watcher) => void | null = null)
#
Methods
zena
notify(): void
#

Called when a watched signal might have changed. Calls the callback given to the constructor unless a subclass overrides it.

zena
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.

zena
unwatch(signal: ReactiveNode): void
#

Stops watching signal. Throws if it is not being watched.

zena
getPending(): GrowableArray<ReactiveNode>
#

The watched computed signals that are stale, in the order they were watched. Reading them brings them up to date.

Effect

zena
class Effect extends Consumer
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.

Constructors
zena
new(fn: () => void | null = null)
#
Methods
zena
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.

zena
dispose(): void
#

Stops the effect: runs its cleanups, disposes the effects it owns, and unsubscribes it from everything it read.

Tracker

zena
final class Tracker extends ReactiveNode
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.

Constructors
zena
new(options: TrackerOptions | null = null)
#
Methods
zena
reportRead(): void
#

Records that the running computed or effect read the value.

zena
reportChanged(): void
#

Marks the computeds and effects that read the value stale. Call it after the value has changed.

Interfaces

Signal

zena
interface Signal<T>

A signal you can read: a State or a Computed.

Methods
zena
get(): T
#

Type aliases

SignalOptions

zena
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

zena
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

zena
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

zena
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

zena
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

zena
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

zena
function isTracking(): boolean

Whether a computed or effect is running and recording what it reads.

currentComputed

zena
function currentComputed(): ReactiveNode | null

The computed or effect that is recording reads, if any.

introspectSources

zena
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

zena
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

zena
function hasSinks(node: ReactiveNode): boolean

Whether node has a live consumer.

hasSources

zena
function hasSources(node: ReactiveNode): boolean

Whether node read any signal on its last run, or watches any.