@pexip/signal
An implementation of Signal and slots which make it easy to implement the observer pattern while avoiding boilerplate code.
Install
npm install @pexip/signal
Signal Variant
Since v0.5.0, there are 2 new types of signal added to the package, behavior
and replay, which are inspired from
rxjs. The
original one is now called as generic.
Here are the diagrams to tell the differences:
Legends
- "Subject" - the subject (data) to emit
- "Observer #" - the observer (a callback function)
- "Behavior" - the behavior version of signal
- "Replay" - the replay version of signal
- "Batched" - the batched version of signal
- "-" - time when something happen
- "*" - the time when the scheduler triggered
generic signal, emit immediately
Signal does not keep any state, and emit the subject immediately for each observer. Observer get what is from the time it is added to the signal stream.
Subject: ---A-----B------C-------D-------- Observer 1: ----C-------D-------- Observer 2: --C-------D-------- Observer 3: -------D--------
behavior signal, always the latest subject
Behavior Signal will always emit the latest value first then it acts as the same as the generic one.
Subject: ---A-----B------C-------D-------- Behavior: ---A-----B------C-------D-------- Observer 1: B---C-------D-------- Observer 2: B-C-------D-------- Observer 3: C------D--------
replay signal with size 2, to replay the recent 2 subjects
Replay Signal will replay the recent number of subject (depends on the
bufferSize), and then act as the same as the generic one.
Subject: ---A-----B------C-------D-------- Replay: ---A----AB-----BC------CD-------- Observer 1: AB--C-------D-------- Observer 2: ABC-------D-------- Observer 3: BC-----D--------
batched signal with size 2, to batch the recent 2 subjects
Batched Signal variant will not emit the subject immediately. Depending on the
provided schedule function which decides the timing of emitting the
subject(s), the Signal keeps buffering the subjects into a buffer with the size
limit by bufferSize (only the latest bufferSize items are kept).
The schedule function can simply be a wrapper of setTimeout or from the
Scheduler API's
postTask to form a time window for batching.
Subject: ---A-----B------C-------D-------- Batched: ---A----AB-----BC------D------- Observer 1: -----------BC-------D---- Observer 2: -----BC-------D---- Observer 3: -------D----
How to signal
Generic Signal
If the variant is not specified, the generic signal will be used. Observer receive what there is since it is added to the signal stream.
const genericSignal = createSignal<number>();
const earlyObserver = (n: number) => console.log(`Early ${n}`);
const lateObserver = (n: number) => console.log(`Late ${n}`);
// early observer subscribed
genericSignal.add(earlyObserver);
genericSignal.emit(100);
genericSignal.emit(101);
// late observer subscribed
genericSignal.add(lateObserver);
genericSignal.emit(102);
// ------------
// Console log
// ------------
// Early 100
// Early 101
// Early 102
// Late 102
Behavior Signal
Always have the latest value when observing the signal.
Here is an example to have the latest number when observing the signal:
import {createSignal} from '@pexip/signal';
const behaviorSignal = createSignal<number>({
variant: 'behavior',
});
const lateToTheParty = (n: number) => console.log(`Late ${n}`);
const superLate = (n: number) => console.log(`Super Late ${n}`);
// first emit when no observers
behaviorSignal.emit(100);
// late to the party observer subscribed
behaviorSignal.add(lateToTheParty);
behaviorSignal.emit(101);
// super late observer subscribed
behaviorSignal.add(superLate);
// ------------
// Console Log
// ------------
// Late 100
// Late 101
// Super Late 101
Replay Signal
Always replay the last number of values when observing the signal.
Here is an example to replay the last 3 numbers when observing the signal:
import {createSignal} from '@pexip/signal';
const behaviorSignal = createSignal<number>({
variant: 'replay',
bufferSize: 3, // default is `2` if it is not specified
});
const lateToTheParty = (n: number) => console.log(`Late ${n}`);
const superLate = (n: number) => console.log(`Super Late ${n}`);
// emits when no observers
behaviorSignal.emit(100);
behaviorSignal.emit(101);
behaviorSignal.emit(102);
behaviorSignal.emit(103);
// late to the party observer subscribed
behaviorSignal.add(lateToTheParty);
behaviorSignal.emit(101);
// super late observer subscribed
behaviorSignal.add(superLate);
// ------------
// Console Log
// ------------
// Late 101
// Late 102
// Late 103
// Late 101
// Super Late 102
// Super Late 103
// Super Late 101
Real world example with React
To communicate between different parts of the application you can create custom signals. For example, we want to get the latest media devices:
// Usually we have a dedicated file to put all the related signals there
// mediaDevicesSignal.ts
import {createSignal} from '@pexip/signal';
export const deviceChangeSignal = createSignal<MediaDeviceInfo[]>({
// we want to have the latest MediaDeviceInfo[] whenever a observer
// subscribing to the signal
variant: 'behavior',
});
We need to subscribe the event somewhere in the app, say in the Meeting
component.
// Meeting Component, Meeting.ts
import {useEffect} form 'react';
import {deviceChangeSignal} from '../mediaDevicesSignal';
function Meeting() {
// Subscribe the event and then emit the signal with the latest list of devices
useEffect(() =>{
let ignore = false;
const updateDeviceList = async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
// Emit the latest device list whenever the `devicechange` event is fired
if (!ignore) {
deviceChangeSignal.emit(devices);
}
};
navigator.mediaDevices.addEventListener('devicechange', updateDeviceList);
return () => {
ignore = true;
navigator.mediaDevices.removeEventListener('devicechange', updateDeviceList);
};
}, []);
return <div>show something here but not important</div>;
}
Now the signal is ready for subscription, you can get the latest list of devices in any component by subscribing the signal.
import {useState, useEffect} from 'react';
import {deviceChangeSignal} from './mediaDevicesSignal';
const SomeComponent = () => {
const [devices, setDevices] = useState<MediaDeviceInfo[]>();
useEffect(() => {
// When ever there is a `deviceChangeSignal` emitted, update the `devices` state
// which causing this component re-rendered and showing the latest list of
// device info
return deviceChangeSignal.add(setDevices);
}, []);
return (
<>
{devices.map(device => (
<div key={device.devicesId}>{device.label}</div>
))}
</>
);
};
Turn off the warning message for non-crucial generic signals
You can turn off the warning when the signal is OK to be missed when it is
emitted and there is no observer which only happened to generic signal.
import {createSignal} from '@pexip/signal';
const crucialSignal = createSignal();
crucialSignal.emit(100);
// ----------------
// Console Warning
// ----------------
// Emitting a signal without any observer! This may be an mistake.
const nonCrucialSignal = createSignal({
allowEmittingWithoutObserver: true,
});
nonCrucialSignal.emit(100); // No warning anymore
Interfaces
| Interface | Description |
|---|---|
| Buffer | Buffer to hold the signal subject |
| Signal | Signal Interface returned from createSignal. |
Type Aliases
| Type Alias | Description |
|---|---|
| BatchedSignalOptions | - |
| BehaviorSignalOptions | - |
| Detach | A detach function to remove its observer from the Signal construct when it is called. |
| ExtractSignalsState | - |
| GenericSignalOptions | - |
| Observer | Generic Observer function to handle the emitted signal. |
| ReplaySignalOptions | - |
| SignalOptions | Signal options |
| SignalVariant | Variant of signal |
Functions
| Function | Description |
|---|---|
| combine | Export operators |
| createSignal | A function to create a new Signal |
| setFlags | Export setLogger for setting logger when consuming the package |
| setLogger | Export setLogger for setting logger when consuming the package |