npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@cdellacqua/signals

v5.0.4

Published

A simple signal pattern implementation that enables reactive programming

Downloads

352

Readme

@cdellacqua/signals

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 Package

npm install @cdellacqua/signals

Documentation

Migrating to V5

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

Highlights

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 and deriving signals

Coalescing

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

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

Readonly signal

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

Adding behaviour

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.