UNPKG

@bett3r-dev/flyd

Version:

The less is more, modular, functional reactive programming library

813 lines (753 loc) 21.1 kB
'use strict'; // 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;