@bett3r-dev/flyd
Version:
The less is more, modular, functional reactive programming library
813 lines (753 loc) • 21.1 kB
JavaScript
;
// Utility
function isFunction(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
}
function trueFn() { return true; }
// Globals
var toUpdate = [];
var inStream;
var order = [];
var orderNextIdx = -1;
var flushingUpdateQueue = false;
var flushingStreamValue = false;
function flushing() {
return flushingUpdateQueue || flushingStreamValue;
}
/** @namespace */
var flyd = {}
// /////////////////////////// API ///////////////////////////////// //
/**
* Creates a new stream
*
* __Signature__: `a -> Stream a`
*
* @name flyd.stream
* @param {*} initialValue - (Optional) the initial value of the stream
* @return {stream} the stream
*
* @example
* var n = flyd.stream(1); // Stream with initial value `1`
* var s = flyd.stream(); // Stream with no initial value
*/
flyd.stream = function(initialValue) {
var endStream = createDependentStream([], trueFn);
var s = createStream();
s.end = endStream;
s.fnArgs = [];
endStream.listeners.push(s);
if (arguments.length > 0) s(initialValue);
return s;
}
// fantasy-land Applicative
flyd.stream['fantasy-land/of'] = flyd.stream.of = flyd.stream;
/**
* Create a new dependent stream
*
* __Signature__: `(...Stream * -> Stream b -> b) -> [Stream *] -> Stream b`
*
* @name flyd.combine
* @param {Function} fn - the function used to combine the streams
* @param {Array<stream>} dependencies - the streams that this one depends on
* @return {stream} the dependent stream
*
* @example
* var n1 = flyd.stream(0);
* var n2 = flyd.stream(0);
* var max = flyd.combine(function(n1, n2, self, changed) {
* return n1() > n2() ? n1() : n2();
* }, [n1, n2]);
*/
function combine(fn, streams) {
if (!streams) return (ss) => combine(fn, ss);
var i, s, deps, depEndStreams;
var endStream = createDependentStream([], trueFn);
deps = []; depEndStreams = [];
for (i = 0; i < streams.length; ++i) {
if (streams[i] !== undefined) {
deps.push(streams[i]);
if (streams[i].end !== undefined) depEndStreams.push(streams[i].end);
}
}
s = createDependentStream(deps, fn);
s.depsChanged = [];
s.fnArgs = s.deps.concat([s, s.depsChanged]);
s.end = endStream;
endStream.listeners.push(s);
addListeners(depEndStreams, endStream);
endStream.deps = depEndStreams;
updateStream(s);
return s;
}
flyd.combine = combine;
/**
* Returns `true` if the supplied argument is a Flyd stream and `false` otherwise.
*
* __Signature__: `* -> Boolean`
*
* @name flyd.isStream
* @param {*} value - the value to test
* @return {Boolean} `true` if is a Flyd streamn, `false` otherwise
*
* @example
* var s = flyd.stream(1);
* var n = 1;
* flyd.isStream(s); //=> true
* flyd.isStream(n); //=> false
*/
flyd.isStream = function(stream) {
return isFunction(stream) && 'hasVal' in stream;
}
/**
* Invokes the body (the function to calculate the value) of a dependent stream
*
* By default the body of a dependent stream is only called when all the streams
* upon which it depends has a value. `immediate` can circumvent this behaviour.
* It immediately invokes the body of a dependent stream.
*
* __Signature__: `Stream a -> Stream a`
*
* @name flyd.immediate
* @param {stream} stream - the dependent stream
* @return {stream} the same stream
*
* @example
* var s = flyd.stream();
* var hasItems = flyd.immediate(flyd.combine(function(s) {
* return s() !== undefined && s().length > 0;
* }, [s]);
* console.log(hasItems()); // logs `false`. Had `immediate` not been
* // used `hasItems()` would've returned `undefined`
* s([1]);
* console.log(hasItems()); // logs `true`.
* s([]);
* console.log(hasItems()); // logs `false`.
*/
flyd.immediate = function(s) {
if (s.depsMet === false) {
s.depsMet = true;
updateStream(s);
}
return s;
}
/**
* Changes which `endsStream` should trigger the ending of `s`.
*
* __Signature__: `Stream a -> Stream b -> Stream b`
*
* @name flyd.endsOn
* @param {stream} endStream - the stream to trigger the ending
* @param {stream} stream - the stream to be ended by the endStream
* @param {stream} the stream modified to be ended by endStream
*
* @example
* var n = flyd.stream(1);
* var killer = flyd.stream();
* // `double` ends when `n` ends or when `killer` emits any value
* var double = flyd.endsOn(flyd.merge(n.end, killer), flyd.combine(function(n) {
* return 2 * n();
* }, [n]);
*/
flyd.endsOn = function(endS, s) {
detachDeps(s.end);
endS.listeners.push(s.end);
s.end.deps.push(endS);
return s;
}
/**
* Map a stream
*
* Returns a new stream consisting of every value from `s` passed through
* `fn`. I.e. `map` creates a new stream that listens to `s` and
* applies `fn` to every new value.
* __Signature__: `(a -> result) -> Stream a -> Stream result`
*
* @name flyd.map
* @param {Function} fn - the function that produces the elements of the new stream
* @param {stream} stream - the stream to map
* @return {stream} a new stream with the mapped values
*
* @example
* var numbers = flyd.stream(0);
* var squaredNumbers = flyd.map(function(n) { return n*n; }, numbers);
*/
// Library functions use self callback to accept (null, undefined) update triggers.
function map(f, s) {
if (!s) return ss => map(f, ss);
return combine(function(s, self) { self(f(s.val)); }, [s]);
}
flyd.map = map;
/**
* Chain a stream
*
* also known as flatMap
*
* Where `fn` returns a stream this function will flatten the resulting streams.
* Every time `fn` is called the context of the returned stream will "switch" to that stream.
*
* __Signature__: `(a -> Stream b) -> Stream a -> Stream b`
*
* @name flyd.chain
* @param {Function} fn - the function that produces the streams to be flattened
* @param {stream} stream - the stream to map
* @return {stream} a new stream with the mapped values
*
* @example
* var filter = flyd.stream('who');
* var items = flyd.chain(function(filter){
* return flyd.stream(findUsers(filter));
* }, filter);
*/
// flyd.chain = curryN(2,chain);
flyd.chain = chain;
/**
* Apply a stream
*
* Applies the value in `s2` to the function in `s1`.
*
* __Signature__: `Stream (a -> b) -> Stream a -> Stream b`
*
* @name flyd.ap
* @param {stream} s1 - The value to be applied
* @param {stream} s2 - The function expecting the value
* @return {stream} a new stream with the mapped values
*
* @example
* var add = stream(a => b => a + b)
* var n1 = stream(1)
* var n2 = stream(2)
*
* var added = flyd.ap(n2, flyd.ap(n1, add)) // stream(3)
* // can also be written using pipe
* var added_pipe = add
* .pipe(ap(n1))
* .pipe(ap(n2));
* added_pipe() // 3
*/
flyd.ap = ap;
/**
* Listen to stream events
*
* Similar to `map` except that the returned stream is empty. Use `on` for doing
* side effects in reaction to stream changes. Use the returned stream only if you
* need to manually end it.
*
* __Signature__: `(a -> result) -> Stream a -> Stream undefined`
*
* @name flyd.on
* @param {Function} cb - the callback
* @param {stream} stream - the stream
* @return {stream} an empty stream (can be ended)
*/
flyd.on = function(f, s) {
if (!s) return ss => flyd.on(f, ss);
return combine(function(s) { f(s.val); }, [s]);
}
/**
* Creates a new stream with the results of calling the function on every incoming
* stream with and accumulator and the incoming value.
*
* __Signature__: `(a -> b -> a) -> a -> Stream b -> Stream a`
*
* @name flyd.scan
* @param {Function} fn - the function to call
* @param {*} val - the initial value of the accumulator
* @param {stream} stream - the stream source
* @return {stream} the new stream
*
* @example
* var numbers = flyd.stream();
* var sum = flyd.scan(function(sum, n) { return sum+n; }, 0, numbers);
* numbers(2)(3)(5);
* sum(); // 10
*/
flyd.scan = function(f, acc, s) {
if ([null, undefined].includes(acc)) return function(acc1, s1) { return flyd.scan(f, acc1, s1)}
else if (!s) return function(s2) { return flyd.scan(f, acc, s2)}
var ns = combine(function(s, self) {
self(acc = f(acc, s.val));
}, [s]);
if (!ns.hasVal) ns(acc);
return ns;
};
/**
* Creates a new stream down which all values from both `stream1` and `stream2`
* will be sent.
*
* __Signature__: `Stream a -> Stream a -> Stream a`
*
* @name flyd.merge
* @param {stream} source1 - one stream to be merged
* @param {stream} source2 - the other stream to be merged
* @return {stream} a stream with the values from both sources
*
* @example
* var btn1Clicks = flyd.stream();
* button1Elm.addEventListener(btn1Clicks);
* var btn2Clicks = flyd.stream();
* button2Elm.addEventListener(btn2Clicks);
* var allClicks = flyd.merge(btn1Clicks, btn2Clicks);
*/
flyd.merge = function(s1, s2) {
if (!s2) return (ss) => flyd.merge(s1, ss);
var s = flyd.immediate(combine(function(s1, s2, self, changed) {
if (changed[0]) {
self(changed[0]());
} else if (s1.hasVal) {
self(s1.val);
} else if (s2.hasVal) {
self(s2.val);
}
}, [s1, s2]));
flyd.endsOn(combine(function() {
return true;
}, [s1.end, s2.end]), s);
return s;
};
/**
* Creates a new stream resulting from applying `transducer` to `stream`.
*
* __Signature__: `Transducer -> Stream a -> Stream b`
*
* @name flyd.transduce
* @param {Transducer} xform - the transducer transformation
* @param {stream} source - the stream source
* @return {stream} the new stream
*
* @example
* var t = require('transducers.js');
*
* var results = [];
* var s1 = flyd.stream();
* var tx = t.compose(t.map(function(x) { return x * 2; }), t.dedupe());
* var s2 = flyd.transduce(tx, s1);
* flyd.combine(function(s2) { results.push(s2()); }, [s2]);
* s1(1)(1)(2)(3)(3)(3)(4);
* results; // => [2, 4, 6, 8]
*/
flyd.transduce = function(xform, source) {
if (!source) return ss => flyd.transduce(xform, ss)
xform = xform(new StreamTransformer());
return combine(function(source, self) {
var res = xform['@@transducer/step'](undefined, source.val);
if (res && res['@@transducer/reduced'] === true) {
self.end(true);
return res['@@transducer/value'];
} else {
return res;
}
}, [source]);
}
/**
* Returns `fn` curried to `n`. Use this function to curry functions exposed by
* modules for Flyd.
*
* @name flyd.curryN
* @function
* @param {Integer} arity - the function arity
* @param {Function} fn - the function to curry
* @return {Function} the curried function
*
* @example
* function add(x, y) { return x + y; };
* var a = flyd.curryN(2, add);
* a(2)(4) // => 6
*/
flyd.curryN = (number, fn) =>
function _curry(...args) {
if (arguments.length >= fn.length) return fn(...args);
return (...argsN)=> _curry(...args, ...argsN);
};
/**
* Returns a new stream identical to the original except every
* value will be passed through `f`.
*
* _Note:_ This function is included in order to support the fantasy land
* specification.
*
* __Signature__: Called bound to `Stream a`: `(a -> b) -> Stream b`
*
* @name stream.map
* @param {Function} function - the function to apply
* @return {stream} a new stream with the values mapped
*
* @example
* var numbers = flyd.stream(0);
* var squaredNumbers = numbers.map(function(n) { return n*n; });
*/
function boundMap(f) { return map(f, this); }
/**
* Returns the result of applying function `fn` to this stream
*
* __Signature__: Called bound to `Stream a`: `(a -> Stream b) -> Stream b`
*
* @name stream.pipe
* @param {Function} fn - the function to apply
* @return {stream} A new stream
*
* @example
* var numbers = flyd.stream(0);
* var squaredNumbers = numbers.pipe(flyd.map(function(n){ return n*n; }));
*/
function operator_pipe(f) { return f(this) }
function boundChain(f) {
return chain(f, this);
}
function chain(f, s) {
// Internal state to end flat map stream
if (!s) return ss => chain(f, ss);
var flatEnd = flyd.stream(1);
var internalEnded = flyd.on(function() {
var alive = flatEnd() - 1;
flatEnd(alive);
if (alive <= 0) {
flatEnd.end(true);
}
});
internalEnded(s.end);
var last = flyd.stream();
var flatStream = flyd.combine(function(s, own) {
last.end(true)
// Our fn stream makes streams
var newS = f(s());
flatEnd(flatEnd() + 1);
internalEnded(newS.end);
// Update self on call -- newS is never handed out so deps don't matter
last = map(own, newS);
}, [s]);
flyd.endsOn(flatEnd.end, flatStream);
return flatStream;
}
flyd.fromPromise = function fromPromise(p) {
var s = flyd.stream();
p.then(function(val) {
s(val);
s.end(true);
});
return s;
}
flyd.flattenPromise = function flattenPromise(s) {
return combine(function(s, self) {
s().then(self);
}, [s])
}
/**
* Returns a new stream which is the result of applying the
* functions from `this` stream to the values in `stream` parameter.
*
* `this` stream must be a stream of functions.
*
* _Note:_ This function is included in order to support the fantasy land
* specification.
*
* __Signature__: Called bound to `Stream (a -> b)`: `a -> Stream b`
*
* @name stream.ap
* @param {stream} stream - the values stream
* @return {stream} a new stream with the functions applied to values
*
* @example
* var add = flyd.curryN(2, function(x, y) { return x + y; });
* var numbers1 = flyd.stream();
* var numbers2 = flyd.stream();
* var addToNumbers1 = flyd.map(add, numbers1);
* var added = addToNumbers1.ap(numbers2);
*/
function ap(s2, s1) {
if (!s1) return ss => ap(s2, ss);
return combine(function(s1, s2, self) { self(s1.val(s2.val)); }, [s1, s2]);
}
function boundAp(s2) {
return ap(s2, this);
}
/**
* @private
*/
function fantasy_land_ap(s1) {
return ap(this, s1);
}
/**
* Get a human readable view of a stream
* @name stream.toString
* @return {String} the stream string representation
*/
function streamToString() {
return 'stream(' + this.val + ')';
}
/**
* @name stream.end
* @memberof stream
* A stream that emits `true` when the stream ends. If `true` is pushed down the
* stream the parent stream ends.
*/
/**
* @name stream.of
* @function
* @memberof stream
* Returns a new stream with `value` as its initial value. It is identical to
* calling `flyd.stream` with one argument.
*
* __Signature__: Called bound to `Stream (a)`: `b -> Stream b`
*
* @param {*} value - the initial value
* @return {stream} the new stream
*
* @example
* var n = flyd.stream(1);
* var m = n.of(1);
*/
// /////////////////////////// PRIVATE ///////////////////////////////// //
/**
* @private
* Create a stream with no dependencies and no value
* @return {Function} a flyd stream
*/
function createStream() {
function s() {
return s._push.apply(this, arguments);
}
s._push = function(n) {
if (arguments.length === 0) return s.val
updateStreamValue(n, s)
return s
}
s.hasVal = false;
s.val = undefined;
s.updaters = [];
s.listeners = [];
s.queued = false;
s.end = undefined;
// fantasy-land compatibility
s.ap = boundAp;
s['fantasy-land/map'] = s.map = boundMap;
s['fantasy-land/ap'] = fantasy_land_ap;
s['fantasy-land/of'] = s.of = flyd.stream;
s['fantasy-land/chain'] = s.chain = boundChain;
s.pipe = operator_pipe;
// According to the fantasy-land Applicative specification
// Given a value f, one can access its type representative via the constructor property:
// `f.constructor.of`
s.constructor = flyd.stream;
s.toJSON = function() {
return s.val;
}
s.toString = streamToString;
return s;
}
/**
* @private
* Create a dependent stream
* @param {Array<stream>} dependencies - an array of the streams
* @param {Function} fn - the function used to calculate the new stream value
* from the dependencies
* @return {stream} the created stream
*/
function createDependentStream(deps, fn) {
var s = createStream();
s.fn = fn;
s.deps = deps;
s.depsMet = false;
s.depsChanged = deps.length > 0 ? [] : undefined;
s.shouldUpdate = false;
addListeners(deps, s);
return s;
}
/**
* @private
* Check if all the dependencies have values
* @param {stream} stream - the stream to check depencencies from
* @return {Boolean} `true` if all dependencies have vales, `false` otherwise
*/
function initialDependenciesMet(stream) {
stream.depsMet = stream.deps.every(function(s) {
return s.hasVal;
});
return stream.depsMet;
}
function dependenciesAreMet(stream) {
return stream.depsMet === true || initialDependenciesMet(stream);
}
function isEnded(stream) {
return stream.end && stream.end.val === true;
}
function listenersNeedUpdating(s) {
return s.listeners.some(function(s) { return s.shouldUpdate; });
}
/**
* @private
* Update a dependent stream using its dependencies in an atomic way
* @param {stream} stream - the stream to update
*/
function updateStream(s) {
if (isEnded(s) || !dependenciesAreMet(s)) return;
if (inStream !== undefined) {
updateLaterUsing(updateStream, s);
return;
}
inStream = s;
if (s.depsChanged) s.fnArgs[s.fnArgs.length - 1] = s.depsChanged;
var returnVal = s.fn.apply(s.fn, s.fnArgs);
if (returnVal !== undefined) {
s(returnVal);
}
inStream = undefined;
if (s.depsChanged !== undefined) s.depsChanged = [];
s.shouldUpdate = false;
if (flushing() === false) flushUpdate();
if (listenersNeedUpdating(s)) {
if (!flushingStreamValue) s(s.val)
else {
s.listeners.forEach(function(listener) {
if (listener.shouldUpdate) updateLaterUsing(updateStream, listener);
});
}
}
}
/**
* @private
* Update the dependencies of a stream
* @param {stream} stream
*/
function updateListeners(s) {
var i, o, list
var listeners = s.listeners;
for (i = 0; i < listeners.length; ++i) {
list = listeners[i];
if (list.end === s) {
endStream(list);
} else {
if (list.depsChanged !== undefined) list.depsChanged.push(s);
list.shouldUpdate = true;
findDeps(list);
}
}
for (; orderNextIdx >= 0; --orderNextIdx) {
o = order[orderNextIdx];
if (o.shouldUpdate === true) updateStream(o);
o.queued = false;
}
}
/**
* @private
* Add stream dependencies to the global `order` queue.
* @param {stream} stream
* @see updateDeps
*/
function findDeps(s) {
var i
var listeners = s.listeners;
if (s.queued === false) {
s.queued = true;
for (i = 0; i < listeners.length; ++i) {
findDeps(listeners[i]);
}
order[++orderNextIdx] = s;
}
}
function updateLaterUsing(updater, stream) {
toUpdate.push(stream);
stream.updaters.push(updater);
stream.shouldUpdate = true;
}
/**
* @private
*/
function flushUpdate() {
flushingUpdateQueue = true;
while (toUpdate.length > 0) {
var stream = toUpdate.shift();
var nextUpdateFn = stream.updaters.shift();
if (nextUpdateFn && stream.shouldUpdate) nextUpdateFn(stream);
}
flushingUpdateQueue = false;
}
/**
* @private
* Push down a value into a stream
* @param {stream} stream
* @param {*} value
*/
function updateStreamValue(n, s) {
s.val = n;
s.hasVal = true;
if (inStream === undefined) {
flushingStreamValue = true;
updateListeners(s);
if (toUpdate.length > 0) flushUpdate();
flushingStreamValue = false;
} else if (inStream === s) {
markListeners(s, s.listeners);
} else {
updateLaterUsing(function(s) { updateStreamValue(n, s); }, s);
}
}
/**
* @private
*/
function markListeners(s, lists) {
var i, list;
for (i = 0; i < lists.length; ++i) {
list = lists[i];
if (list.end !== s) {
if (list.depsChanged !== undefined) {
list.depsChanged.push(s);
}
list.shouldUpdate = true;
} else {
endStream(list);
}
}
}
/**
* @private
* Add dependencies to a stream
* @param {Array<stream>} dependencies
* @param {stream} stream
*/
function addListeners(deps, s) {
for (var i = 0; i < deps.length; ++i) {
deps[i].listeners.push(s);
}
}
/**
* @private
* Removes an stream from a dependency array
* @param {stream} stream
* @param {Array<stream>} dependencies
*/
function removeListener(s, listeners) {
var idx = listeners.indexOf(s);
listeners[idx] = listeners[listeners.length - 1];
listeners.length--;
}
/**
* @private
* Detach a stream from its dependencies
* @param {stream} stream
*/
function detachDeps(s) {
for (var i = 0; i < s.deps.length; ++i) {
removeListener(s, s.deps[i].listeners);
}
s.deps.length = 0;
}
/**
* @private
* Ends a stream
*/
function endStream(s) {
if (s.deps !== undefined) detachDeps(s);
if (s.end !== undefined) detachDeps(s.end);
}
/**
* @private
*/
/**
* @private
* transducer stream transformer
*/
function StreamTransformer() { }
StreamTransformer.prototype['@@transducer/init'] = function() { };
StreamTransformer.prototype['@@transducer/result'] = function() { };
StreamTransformer.prototype['@@transducer/step'] = function(s, v) { return v; };
module.exports = flyd;