There is a very helpful guide Signals vs. Observables at builder.io contrasting the different semantics of signals and observables. Whilst it draws the helpful distinction that signals are "pull" based whereas observables are "push" based it seems there is an important space of semantic, apparently dependent on this design choice, that it does not consider. In the preact-signals test cases at signal.test.tsx there is a significant sequence which verify behaviour in cases where flow graphs rejoin, creating patterns shown in the ASCII art as "flags", "diamonds", etc. The test cases verify an important (to me) data consistency property, that regardless of the shape of the flow graph, upstream observers receive only one notification for each downstream change, and see a "consistent data horizon" where, for example, in the flag pattern, B has been notified definitely before A.
It seems that, if the distinction between "push-based" and "pull-based" reactive value systems is valid, "push-based" systems do not enjoy this property, and "pull-based" systems do not advertise it particularly clearly as a benefit, making it hard to see whether they all do.
Questions -
Here are two worked examples of a "flag-like" pattern in RxJS ("push-based" observables) and preact-signals ("pull-based" signals) where there are two paths of different lengths in the flow graph between the producer and consumer of the values.
In each case we have HTML of a simple button to trigger the changes
<div>
<button id='button'>Button</button>
</div>
Firstly with RxJS:
import { fromEvent, combineLatest } from 'rxjs';
import { scan, map, startWith } from 'rxjs/operators';
const buttonElem = document.getElementById('button');
const clickCount = fromEvent(buttonElem, 'click').pipe(
scan((count) => count + 1, 0),
startWith(0)
);
const derived = clickCount.pipe(map((count) => count + 0.5));
const combiner = map(([clickCount, derived]) => ({ clickCount, derived }));
let triggerCount = 0;
const combinedObs = combineLatest([clickCount, derived]).pipe(combiner);
combinedObs.subscribe(combined => {
++triggerCount;
console.log('Trigger count ', triggerCount, ' combined ', combined);
});
Clicking the button once shows that we get two notifications to the subscriber, where the first one has "janked":
Trigger count 1 combined {clickCount: 0, derived: 0.5}
Trigger count 2 combined {clickCount: 1, derived: 0.5}
Trigger count 3 combined {clickCount: 1, derived: 1.5}
Could this have been avoided with a different choice of RxJS operator? I know there is a choice for "zip" but this assumes that all consumed signals will change synchronously and doesn't allow for combining values some of which have changed at one time and some at another.
Working the same example with preact-signals:
import { signal, computed, effect } from "@preact/signals-core";
const buttonElem = document.getElementById('button');
const clickCount = signal(0);
buttonElem.addEventListener("click", () => ++clickCount.value);
const derived = computed( () => clickCount.value + 0.5);
const combined = computed( () => ({clickCount: clickCount.value, derived: derived.value}));
let triggerCount = 0;
effect( () => {
++triggerCount;
console.log('Trigger count ', triggerCount, ' combined ', combined.value);
});
Produces the following output without jank:
Trigger count 1 combined {clickCount: 0, derived: 0.5}
Trigger count 2 combined {clickCount: 1, derived: 1.5}
Can this beneficial property be taken for granted amongst signals implementations and if not, which ones enjoy it?
The industry's standard term for this phenomenon is "glitch", introduced into the literature in Cooper & Krishnamurthi's 2006 paper on their reactive system, FrTime. They define a glitch as
where a signal is recomputed before all of its subordinate signals are up-to-date
The major review paper Bainomugisha et al, 2012 A Survey on Reactive Programming catalogues 15 reactive systems of which 4 are found to be glitchy. All of the sound libraries feature some form of "pull" based workflow.
The "push vs pull" distinction is mostly valid in that pure push-based reactive systems are likely to glitch. In practice the modern commodity signals algorithms such as preact-signals use a mixed "push + pull" strategy to optimise traversal of the invalidated graph whilst preserving glitch freedom - these strategies are explained by Ryan Carniato and Reactively.
RxJS is a now relatively rare example of an irredeemably glitchy library as explained in this answer - how to avoid glitches in Rx - it's puzzling what use cases it might be aimed at.
There's a standard posting on this subject here: Terminology: What is a "glitch" in Functional Reactive Programming / RX?