live-set
![npm version](https://badge.fury.io/js/live-set.svg)
This class represents a set of values which may change over time or have
transformations applied to it, resulting in a new LiveSet. After modifications
are made, notifications will be delivered asynchronously, like Javascript
Promises or
MutationObservers
do. This library is inspired by the Kefir
library and the Observable
proposal, but represents a
changing set rather than a stream of a single updating value.
Like Kefir Observables, LiveSets have active and inactive states and start out
in the inactive state. When a LiveSet first gets a subscriber, the LiveSet
becomes active and calls the listen function provided to it. When a LiveSet no
longer has any subscribers, it becomes deactivated. Unlike Kefir Observables,
events are delivered asynchronously, and a LiveSet instance may have its
current values queried at any time.
Here's an example of a liveset being made to represent the direct children of
an HTMLElement, using a MutationObserver to keep the liveset updated:
import LiveSet from 'live-set';
function createElementChildLiveSet(element) {
return new LiveSet({
read() {
return new Set(Array.from(element.children));
},
listen(setValues, controller) {
setValues(this.read());
function changesHandler(mutations) {
mutations.forEach(mutation => {
Array.prototype.forEach.call(mutation.addedNodes, child => {
if (child.nodeType === 1) controller.add(child);
});
Array.prototype.forEach.call(mutation.removedNodes, child => {
if (child.nodeType === 1) controller.remove(child);
});
});
}
const observer = new MutationObserver(changesHandler);
observer.observe(element, {childList: true});
return {
unsubscribe() {
observer.disconnect();
},
pullChanges() {
changesHandler(observer.takeRecords());
}
};
}
});
}
const bodyChildren = createElementChildLiveSet(document.body);
console.log(bodyChildren.values());
const subscription = bodyChildren.subscribe(changes => {
console.log(changes);
});
subscription.unsubscribe();
The LiveSet instance could then be passed around, read, and subscribed to. New
LiveSets can be created by transforming existing LiveSets. Here's an example
building off of the above example:
import filter from 'live-set/filter';
const bodyChildrenNoDivs = filter(bodyChildren, el => el.nodeName !== 'DIV');
console.log(bodyChildrenNoDivs.values());
import flatMap from 'live-set/flatMap';
const bodyChildrenNoDivsChildren = flatMap(bodyChildrenNoDivs, el => createElementChildLiveSet(el));
Like Kefir Observables, the activation model means that intermediate LiveSets
can be created and consumed without needing to be explicitly cleaned up after
the output liveset is unsubscribed from. Consider the following code in which
several livesets are created from other livesets:
import LiveSet from 'live-set';
import map from 'live-set/map';
import merge from 'live-set/merge';
const i1 = new LiveSet({
read() {
throw new Error('not implemented');
},
listen(setValues, controller) {
setValues(new Set([5]));
let i = 6;
const t = setInterval(() => {
controller.add(i);
i++;
}, 1000);
return () => {
clearInterval(t);
};
}
});
const i2 = map(i1, x => x*10);
const final = merge([
i2,
LiveSet.constant(new Set([1]))
]);
const subscription = final.subscribe({
start() {
console.log('All values', Array.from(final.values()));
},
next(changes) {
console.log('changes', changes);
}
});
setTimeout(() => {
subscription.unsubscribe();
}, 3500);
The ability to read the values of an inactive LiveSet is provided for
convenience, but in some situations it has surprising results if
transformations are not pure (including the case where they instantiate objects
and their identities are depended on). Consider the following code:
import LiveSet from 'live-set';
import map from 'live-set/map';
const input = LiveSet.constant(new Set([5, 6]));
const mapped = map(input, x => ({value: x}));
const firstValue1 = Array.from(mapped.values())[0];
console.log(firstValue1);
const firstValue2 = Array.from(mapped.values())[0];
console.log(firstValue2);
console.log(firstValue1 === firstValue2);
The mapped
LiveSet while inactive does not keep a cache of the results of the
transformed input values. It could only know to remove them if it subscribed
to the input liveset, but that could cause input
to keep resources open. The
mapped
LiveSet will only become active and trigger a subscribtion to input
if it is subscribed to first. Here we subscribe to it with an empty observer to
demonstrate the difference:
import LiveSet from 'live-set';
import map from 'live-set/map';
const input = LiveSet.constant(new Set([5, 6]));
const mapped = map(input, x => ({value: x}));
mapped.subscribe({});
const firstValue1 = Array.from(mapped.values())[0];
const firstValue2 = Array.from(mapped.values())[0];
console.log(firstValue1 === firstValue2);
API
Core
LiveSet::constructor
LiveSet<T>::constructor({read, listen})
LiveSet.constant
LiveSet.constant<T>(values: Set<T>): LiveSet<T>
LiveSet.active
LiveSet.active<T>(initialValues?: Set<T>): {liveSet: LiveSet<T>, controller: LiveSetController<T>}
LiveSet::isEnded
LiveSet<T>::isEnded(): boolean
LiveSet::values
LiveSet<T>::values(): Set<T>
LiveSet::subscribe
LiveSet<T>::subscribe(observer): LiveSetSubscription
LiveSetSubscription::closed
LiveSetSubscription::closed: boolean
LiveSetSubscription::unsubscribe
LiveSetSubscription::unsubscribe(): void
LiveSetSubscription::pullChanges
LiveSetSubscription::pullChanges(): void
Transformations
The following functions usually take a pre-existing LiveSet instance as input,
and return a new LiveSet instance. These functions are implemented in separate
modules rather than as methods of LiveSet in part so that only the functions
used have to be included in a javascript bundle built for browsers.
live-set/filter
filter<T>(liveSet: LiveSet<T>, cb: (value: T) => any): LiveSet<T>
live-set/map
map<T,U>(liveSet: LiveSet<T>, cb: (value: T) => U): LiveSet<U>
live-set/transduce
transduce(liveSet: LiveSet<any>, transducer: Function): LiveSet<any>
live-set/merge
merge<T>(liveSets: Array<LiveSet<T>>): LiveSet<T>
live-set/flatMap
flatMap<T,U>(liveSet: LiveSet<T>, cb: (value: T) => LiveSet<U>): LiveSet<U>
live-set/mapWithRemoval
mapWithRemoval<T,U>(input: LiveSet<T>, cb: (value: T, removal: Promise<void>) => U): LiveSet<U>
live-set/toValueObservable
toValueObservable<T>(liveSet: LiveSet<T>): Observable<{value: T, removal: Promise<void>}>
Types
Flow type declarations for this module are included!
If you are using Flow, they won't require any configuration to use.