UNPKG

flyd

Version:

The less is more, modular, functional reactive programming library

1,003 lines (926 loc) 25.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.flyd = factory()); }(this, (function () { 'use strict'; function _arity(n, fn) { /* eslint-disable no-unused-vars */ switch (n) { case 0: return function () { return fn.apply(this, arguments); }; case 1: return function (a0) { return fn.apply(this, arguments); }; case 2: return function (a0, a1) { return fn.apply(this, arguments); }; case 3: return function (a0, a1, a2) { return fn.apply(this, arguments); }; case 4: return function (a0, a1, a2, a3) { return fn.apply(this, arguments); }; case 5: return function (a0, a1, a2, a3, a4) { return fn.apply(this, arguments); }; case 6: return function (a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); }; case 7: return function (a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); }; case 8: return function (a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); }; case 9: return function (a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); }; case 10: return function (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); }; default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten'); } } var _arity_1 = _arity; function _isPlaceholder(a) { return a != null && typeof a === 'object' && a['@@functional/placeholder'] === true; } var _isPlaceholder_1 = _isPlaceholder; /** * Optimized internal one-arity curry function. * * @private * @category Function * @param {Function} fn The function to curry. * @return {Function} The curried function. */ function _curry1(fn) { return function f1(a) { if (arguments.length === 0 || _isPlaceholder_1(a)) { return f1; } else { return fn.apply(this, arguments); } }; } var _curry1_1 = _curry1; /** * Optimized internal two-arity curry function. * * @private * @category Function * @param {Function} fn The function to curry. * @return {Function} The curried function. */ function _curry2(fn) { return function f2(a, b) { switch (arguments.length) { case 0: return f2; case 1: return _isPlaceholder_1(a) ? f2 : _curry1_1(function (_b) { return fn(a, _b); }); default: return _isPlaceholder_1(a) && _isPlaceholder_1(b) ? f2 : _isPlaceholder_1(a) ? _curry1_1(function (_a) { return fn(_a, b); }) : _isPlaceholder_1(b) ? _curry1_1(function (_b) { return fn(a, _b); }) : fn(a, b); } }; } var _curry2_1 = _curry2; /** * Internal curryN function. * * @private * @category Function * @param {Number} length The arity of the curried function. * @param {Array} received An array of arguments received thus far. * @param {Function} fn The function to curry. * @return {Function} The curried function. */ function _curryN(length, received, fn) { return function () { var combined = []; var argsIdx = 0; var left = length; var combinedIdx = 0; while (combinedIdx < received.length || argsIdx < arguments.length) { var result; if (combinedIdx < received.length && (!_isPlaceholder_1(received[combinedIdx]) || argsIdx >= arguments.length)) { result = received[combinedIdx]; } else { result = arguments[argsIdx]; argsIdx += 1; } combined[combinedIdx] = result; if (!_isPlaceholder_1(result)) { left -= 1; } combinedIdx += 1; } return left <= 0 ? fn.apply(this, combined) : _arity_1(left, _curryN(length, combined, fn)); }; } var _curryN_1 = _curryN; /** * Returns a curried equivalent of the provided function, with the specified * arity. The curried function has two unusual capabilities. First, its * arguments needn't be provided one at a time. If `g` is `R.curryN(3, f)`, the * following are equivalent: * * - `g(1)(2)(3)` * - `g(1)(2, 3)` * - `g(1, 2)(3)` * - `g(1, 2, 3)` * * Secondly, the special placeholder value [`R.__`](#__) may be used to specify * "gaps", allowing partial application of any combination of arguments, * regardless of their positions. If `g` is as above and `_` is [`R.__`](#__), * the following are equivalent: * * - `g(1, 2, 3)` * - `g(_, 2, 3)(1)` * - `g(_, _, 3)(1)(2)` * - `g(_, _, 3)(1, 2)` * - `g(_, 2)(1)(3)` * - `g(_, 2)(1, 3)` * - `g(_, 2)(_, 3)(1)` * * @func * @memberOf R * @since v0.5.0 * @category Function * @sig Number -> (* -> a) -> (* -> a) * @param {Number} length The arity for the returned function. * @param {Function} fn The function to curry. * @return {Function} A new, curried function. * @see R.curry * @example * * var sumArgs = (...args) => R.sum(args); * * var curriedAddFourNumbers = R.curryN(4, sumArgs); * var f = curriedAddFourNumbers(1, 2); * var g = f(3); * g(4); //=> 10 */ var curryN = /*#__PURE__*/_curry2_1(function curryN(length, fn) { if (length === 1) { return _curry1_1(fn); } return _arity_1(length, _curryN_1(length, [], fn)); }); var curryN_1 = curryN; // 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]); */ flyd.combine = curryN_1(2, combine); function combine(fn, streams) { 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; } /** * 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) { return combine(function(s, self) { self(f(s.val)); }, [s]); } flyd.map = curryN_1(2, 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_1(2, 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 = curryN_1(2, 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 = curryN_1(2, function(f, s) { 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 = curryN_1(3, function(f, acc, s) { 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 = curryN_1(2, function(s1, s2) { 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 = curryN_1(2, function(xform, source) { 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 = curryN_1; /** * 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 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) { 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(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; }; var lib = flyd; return lib; })));