Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
@cdellacqua/signals
Advanced tools
A simple signal pattern implementation that enables reactive programming
A simple signal pattern implementation that enables reactive programming.
Signals are event emitters with specific purposes. For example:
button.addEventListener('click', () => console.log('click'));
input.addEventListener('change', (e) => console.log(e));
...could be rewritten with signals as:
button.clicked.subscribe(() => console.log('click'));
input.changed.subscribe((e) => console.log(e));
npm install @cdellacqua/signals
TL;DR: replace nOfSubscriptions to nOfSubscriptions().
The only major change is the refactoring of nOfSubscriptions. Up until V4 it was a getter property, in V5 it's a function.
This change is meant to prevent common pitfalls that occur when composing signals in custom objects. As an example, when using {...signal$, myCustomExtension() { /* my code */ } }
, the
object spread syntax would previously capture the current value returned by the
getter, making the field a regular object property that couldn't update on its own.
It's now possible to use the spread syntax, because it will capture the function
instead of the current value.
A positive side effect of this change is the reduced number of function calls necessary to reach the value hidden behind the getter (i.e. nOfSubscriptions doesn't need to be redefined as a getter in every composite object, it just needs to be a reference to the original function).
Signal<T>
provides methods such as:
emit(value)
, to emit a value to all subscribers;subscribe(subscriber)
, to attach subscribers;subscribeOnce(subscriber)
, to attach subscribers for a single emit
call.When you subscribe to a signal, you get a unsubscribe function, e.g.:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const unsubscribe = signal$.subscribe((v) => console.log(v));
signal$.emit(3.14); // will trigger console.log, printing 3.14
unsubscribe();
signal$.emit(42); // won't do anything
The above code can be rewritten with subscribeOnce
:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
signal$.subscribeOnce((v) => console.log(v));
signal$.emit(3.14); // will trigger console.log, printing 3.14
signal$.emit(42); // won't do anything
Signal<T>
also contains a getter (nOfSubscriptions
) that lets you know how many active subscriptions
are active at a given moment (this could be useful if you are trying to optimize your code).
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
console.log(signal$.nOfSubscriptions()); // 0
const unsubscribe = signal$.subscribe(() => undefined); // empty subscriber
console.log(signal$.nOfSubscriptions()); // 1
unsubscribe();
console.log(signal$.nOfSubscriptions()); // 0
A nice feature of Signal<T>
is that it deduplicates subscribers,
that is you can't accidentally add the same function more than
once to the same signal (just like the DOM addEventListener method):
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const subscriber = (v: number) => console.log(v);
console.log(signal$.nOfSubscriptions()); // 0
const unsubscribe1 = signal$.subscribe(subscriber);
const unsubscribe2 = signal$.subscribe(subscriber);
const unsubscribe3 = signal$.subscribe(subscriber);
console.log(signal$.nOfSubscriptions()); // 1
unsubscribe3(); // will remove "subscriber"
unsubscribe2(); // won't do anything, "subscriber" has already been removed
unsubscribe1(); // won't do anything, "subscriber" has already been removed
console.log(signal$.nOfSubscriptions()); // 0
If you ever needed to add the same function more than once you can still achieve that by simply wrapping it inside an arrow function:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const subscriber = (v: number) => console.log(v);
console.log(signal$.nOfSubscriptions()); // 0
const unsubscribe1 = signal$.subscribe(subscriber);
console.log(signal$.nOfSubscriptions()); // 1
const unsubscribe2 = signal$.subscribe((v) => subscriber(v));
console.log(signal$.nOfSubscriptions()); // 2
unsubscribe2();
console.log(signal$.nOfSubscriptions()); // 1
unsubscribe1();
console.log(signal$.nOfSubscriptions()); // 0
You can also have a signal that just triggers its subscribers without passing any data:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<void>();
signal$.emit();
Coalescing multiple signals into one consists of creating a new signal that will emit the latest value emitted by any source signal.
Example:
import {makeSignal, coalesceSignals} from '@cdellacqua/signals';
const lastUpdate1$ = makeSignal<number>();
const lastUpdate2$ = makeSignal<number>();
const latestUpdate$ = coalesceSignals([lastUpdate1$, lastUpdate2$]);
latestUpdate$.subscribe((v) => console.log(v));
lastUpdate1$.emit(1577923200000); // will log 1577923200000
lastUpdate2$.emit(1653230659450); // will log 1653230659450
Deriving a signal consists of creating a new signal that emits a value mapped from the source signal.
Example:
import {makeSignal, deriveSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const derived$ = deriveSignal(signal$, (n) => n + 100);
derived$.subscribe((v) => console.log(v));
signal$.emit(3); // will trigger console.log, echoing 103
When you coalesce or derive a signal, you get back a ReadonlySignal<T>
.
This type lacks the emit
method.
A Signal<T>
is in fact an extension of a ReadonlySignal<T>
that adds the emit
method.
As a rule of thumb, it is preferable to pass around ReadonlySignal<T>
s,
to better encapsulate your signals and prevent unwanted emit
s.
If you need to encapsulate behaviour in a custom signal, you can simply destructure a regular signal and add your custom methods to the already existing ones.
Example:
import {makeSignal} from '@cdellacqua/signals';
const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms));
function makeCountdown(from: number): ReadonlySignal<number> & {run(): Promise<void>} {
const {subscribe, subscribeOnce, emit, nOfSubscriptions} = makeSignal<number>();
return {
subscribe,
subscribeOnce,
nOfSubscriptions,
async run() {
emit(from);
for (let i = from - 1; i >= 0; i--) {
await sleep(1000);
emit(i);
}
},
};
}
const countdown$ = makeCountdown(5);
countdown$.subscribe(console.log);
countdown$.run().then(() => console.log('launch!')); // will trigger the above console.log 6 times, printing the numbers from 5 to 0.
FAQs
A simple signal pattern implementation that enables reactive programming
We found that @cdellacqua/signals demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.