signals
js/signals.ts
fino:signals — retained reactive values for runtime read models.
Signals model state: the latest value is retained, subscribers are notified
when that value changes, and Object.is skips redundant writes. They are a
good fit for UI render dependencies, queue statistics, run status, counters,
and other "what is true now" views. They are not an event log; streams,
topics, and async iterators remain the right surface when every intermediate
value matters.
Design
Writable Signal instances are owned by producers. Public APIs should expose
ReadonlySignal when consumers must observe but not update the value.
batch() coalesces synchronous writes into one notification pass, while
computed() and effect() use observeReads() to track whichever signals
were read on the latest run.
lazy() and fromIterable() are cold bridges: upstream work starts only
when the first subscriber attaches and is disposed after the last subscriber
leaves. The current value remains readable even while the producer is cold.
import { computed, createSignal, effect } from 'fino:signals';
const count = createSignal(0);
const doubled = computed(() => count.get() * 2);
const stop = effect(() => {
console.log(doubled.get());
});
count.set(2);
stop();
Types
type SignalSubscriber<T> = (value: T, previous: T) => void
Callback invoked after a signal value changes.
The callback receives the current value and the previous value. Batched writes call subscribers once with the value before the first write and the final value after the outermost batch completes.
type SignalSetter<T> = T | ((value: T) => T)
Value or updater accepted by Signal.set().
Interfaces
interface ReadonlySignal<T> {
Read-only signal handle for consumers.
A ReadonlySignal exposes the current value and change notifications but
does not allow callers to write. Producers should prefer this shape for
public runtime read models.
Methods
get(): T
Return the current retained value.
subscribe(subscriber: SignalSubscriber<T>): () => void
Subscribe to future value changes.
The callback is not called immediately. The returned function removes the subscription and is safe to call more than once.
interface ObservedReads<T> {
Result returned by observeReads().
Properties
value: T
Value returned by the observed callback.
signals: ReadonlySignal < unknown > []
Unique signals read while the callback ran, in first-read order.
Functions
function batch<T>(fn: () => T): T
Run multiple signal writes as one notification pass.
Subscribers are called after the outermost batch completes, and each changed signal notifies at most once with its final value.
function createSignal<T>(initial: T): Signal<T>
Create a writable signal with explicit get, set, and subscribe methods.
function observeReads<T>(fn: () => T): ObservedReads<T>
Run fn and return the signals read during that run.
This is the low-level hook used by renderers and reactive helpers. Nested calls are isolated, and duplicate reads of the same signal are reported once.
function computed<T>(fn: () => T): ReadonlySignal<T>
Create a read-only signal derived from other signals.
The derivation runs immediately, then re-runs whenever any signal read during the latest run changes. Conditional dependencies are re-tracked each time.
function effect(fn: () => void): () => void
Run a side effect now and whenever its read dependencies change.
The returned function disposes the current dependencies and prevents future re-runs. Dependencies are re-tracked on every execution.
function lazy<T>(initial: T, start: (set: (value: T) => void) => () => void): ReadonlySignal<T>
Create a cold read-only signal.
start is called when the first subscriber attaches. It receives a setter for
publishing retained values and returns a cleanup callback, which runs after
the last subscriber unsubscribes.
function fromIterable<
T,
S
>(src: AsyncIterable<T>, fold: (acc: S, item: T) => S, initial: S): ReadonlySignal<S>
Fold an async iterable into a cold retained signal.
The iterable is consumed only while the signal has subscribers. On teardown,
the active iterator's return() method is called when present so upstream
subscriptions and readers can release resources.
Classes
class Signal<T> implements ReadonlySignal<T> {
Explicit mutable reactive value.
Use get() to read the retained value, set() to replace or derive the
next value, and subscribe() to observe later changes. Redundant writes are
skipped with Object.is.
Constructors
constructor(initial: T)
Methods
get(): T
Return the current signal value and record the read when observed.
set(next: SignalSetter<T>): void
Replace the value or derive the next value from the current one.
Subscribers are skipped when Object.is(previous, next) is true.
subscribe(subscriber: SignalSubscriber<T>): () => void
Subscribe to value changes.
The returned function removes the subscriber. Subscriptions do not fire immediately; they only observe subsequent writes.