|
/** |
|
|
|
The MIT License (MIT) |
|
|
|
Copyright (c) 2015 Simon Friis Vindum |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
of this software and associated documentation files (the "Software"), to deal |
|
in the Software without restriction, including without limitation the rights |
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
copies of the Software, and to permit persons to whom the Software is |
|
furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included in all |
|
copies or substantial portions of the Software. |
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
SOFTWARE. |
|
|
|
*/ |
|
|
|
(function (root, factory) { |
|
if (typeof define === 'function' && define.amd) { |
|
define([], factory); // AMD. Register as an anonymous module. |
|
} else if (typeof exports === 'object') { |
|
module.exports = factory(); // NodeJS |
|
} else { // Browser globals (root is window) |
|
root.flyd = factory(); |
|
} |
|
}(this, function () { |
|
|
|
'use strict'; |
|
|
|
function isFunction(obj) { |
|
return !!(obj && obj.constructor && obj.call && obj.apply); |
|
} |
|
|
|
// Globals |
|
var toUpdate = []; |
|
var inStream; |
|
|
|
function map(f, s) { |
|
return stream([s], function(self) { self(f(s())); }); |
|
} |
|
|
|
function boundMap(f) { return map(f, this); } |
|
|
|
var scan = curryN(3, function(f, acc, s) { |
|
var ns = stream([s], function() { |
|
return (acc = f(acc, s())); |
|
}); |
|
if (!ns.hasVal) ns(acc); |
|
return ns; |
|
}); |
|
|
|
var merge = curryN(2, function(s1, s2) { |
|
var s = immediate(stream([s1, s2], function(n, changed) { |
|
return changed[0] ? changed[0]() |
|
: s1.hasVal ? s1() |
|
: s2(); |
|
})); |
|
endsOn(stream([s1.end, s2.end], function(self, changed) { |
|
return true; |
|
}), s); |
|
return s; |
|
}); |
|
|
|
function ap(s2) { |
|
var s1 = this; |
|
return stream([s1, s2], function() { return s1()(s2()); }); |
|
} |
|
|
|
function initialDepsNotMet(stream) { |
|
stream.depsMet = stream.deps.every(function(s) { |
|
return s.hasVal; |
|
}); |
|
return !stream.depsMet; |
|
} |
|
|
|
function updateStream(s) { |
|
if ((s.depsMet !== true && initialDepsNotMet(s)) || |
|
(s.end !== undefined && s.end.val === true)) return; |
|
inStream = s; |
|
var returnVal = s.fn(s, s.depsChanged); |
|
if (returnVal !== undefined) { |
|
s(returnVal); |
|
} |
|
inStream = undefined; |
|
if (s.depsChanged !== undefined) { |
|
while (s.depsChanged.length > 0) s.depsChanged.shift(); |
|
} |
|
s.shouldUpdate = false; |
|
} |
|
|
|
var order = []; |
|
var orderNextIdx = -1; |
|
|
|
function findDeps(s) { |
|
var i, listeners = s.listeners; |
|
if (s.queued === false) { |
|
s.queued = true; |
|
for (i = 0; i < listeners.length; ++i) { |
|
findDeps(listeners[i]); |
|
} |
|
order[++orderNextIdx] = s; |
|
} |
|
} |
|
|
|
function updateDeps(s) { |
|
var i, o, list, 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; |
|
} |
|
} |
|
|
|
function flushUpdate() { |
|
while (toUpdate.length > 0) updateDeps(toUpdate.shift()); |
|
} |
|
|
|
function isStream(stream) { |
|
return isFunction(stream) && 'hasVal' in stream; |
|
} |
|
|
|
function streamToString() { |
|
return 'stream(' + this.val + ')'; |
|
} |
|
|
|
function createStream() { |
|
function s(n) { |
|
var i, list; |
|
if (arguments.length === 0) { |
|
return s.val; |
|
} else { |
|
if (n !== undefined && n !== null && isFunction(n.then)) { |
|
n.then(s); |
|
return; |
|
} |
|
s.val = n; |
|
s.hasVal = true; |
|
if (inStream === undefined) { |
|
updateDeps(s); |
|
if (toUpdate.length > 0) flushUpdate(); |
|
} else if (inStream === s) { |
|
for (i = 0; i < s.listeners.length; ++i) { |
|
list = s.listeners[i]; |
|
if (list.end !== s) { |
|
if (list.depsChanged !== undefined) { |
|
list.depsChanged.push(s); |
|
} |
|
list.shouldUpdate = true; |
|
} else { |
|
endStream(list); |
|
} |
|
} |
|
} else { |
|
toUpdate.push(s); |
|
} |
|
return s; |
|
} |
|
} |
|
s.hasVal = false; |
|
s.val = undefined; |
|
s.listeners = []; |
|
s.queued = false; |
|
s.end = undefined; |
|
|
|
s.map = boundMap; |
|
s.ap = ap; |
|
s.of = stream; |
|
s.toString = streamToString; |
|
|
|
return s; |
|
} |
|
|
|
function createDependentStream(deps, fn) { |
|
var i, s = createStream(); |
|
s.fn = fn; |
|
s.deps = deps; |
|
s.depsMet = false; |
|
s.depsChanged = fn.length > 1 ? [] : undefined; |
|
s.shouldUpdate = false; |
|
for (i = 0; i < deps.length; ++i) { |
|
deps[i].listeners.push(s); |
|
} |
|
return s; |
|
} |
|
|
|
function immediate(s) { |
|
if (s.depsMet === false) { |
|
s.depsMet = true; |
|
updateStream(s); |
|
if (toUpdate.length > 0) flushUpdate(); |
|
} |
|
return s; |
|
} |
|
|
|
function removeListener(s, listeners) { |
|
var idx = listeners.indexOf(s); |
|
listeners[idx] = listeners[listeners.length - 1]; |
|
listeners.length--; |
|
} |
|
|
|
function detachDeps(s) { |
|
for (var i = 0; i < s.deps.length; ++i) { |
|
removeListener(s, s.deps[i].listeners); |
|
} |
|
s.deps.length = 0; |
|
} |
|
|
|
function endStream(s) { |
|
if (s.deps !== undefined) detachDeps(s); |
|
if (s.end !== undefined) detachDeps(s.end); |
|
} |
|
|
|
function endsOn(endS, s) { |
|
detachDeps(s.end); |
|
endS.listeners.push(s.end); |
|
s.end.deps.push(endS); |
|
return s; |
|
} |
|
|
|
function stream(arg, fn) { |
|
var i, s, deps, depEndStreams; |
|
var endStream = createDependentStream([], function() { return true; }); |
|
if (arguments.length > 1) { |
|
deps = []; depEndStreams = []; |
|
for (i = 0; i < arg.length; ++i) { |
|
if (arg[i] !== undefined) { |
|
deps.push(arg[i]); |
|
if (arg[i].end !== undefined) depEndStreams.push(arg[i].end); |
|
} |
|
} |
|
s = createDependentStream(deps, fn); |
|
s.end = endStream; |
|
endStream.listeners.push(s); |
|
endsOn(createDependentStream(depEndStreams, function() { return true; }, true), s); |
|
updateStream(s); |
|
if (toUpdate.length > 0) flushUpdate(); |
|
} else { |
|
s = createStream(); |
|
s.end = endStream; |
|
endStream.listeners.push(s); |
|
if (arguments.length === 1) s(arg); |
|
} |
|
return s; |
|
} |
|
|
|
var transduce = curryN(2, function(xform, source) { |
|
xform = xform(new StreamTransformer()); |
|
return stream([source], function(self) { |
|
var res = xform['@@transducer/step'](undefined, source()); |
|
if (res && res['@@transducer/reduced'] === true) { |
|
self.end(true); |
|
return res['@@transducer/value']; |
|
} else { |
|
return res; |
|
} |
|
}); |
|
}); |
|
|
|
function StreamTransformer() { } |
|
StreamTransformer.prototype['@@transducer/init'] = function() { }; |
|
StreamTransformer.prototype['@@transducer/result'] = function() { }; |
|
StreamTransformer.prototype['@@transducer/step'] = function(s, v) { return v; }; |
|
|
|
// Own curry implementation snatched from Ramda |
|
// Figure out something nicer later on |
|
var _ = {placeholder: true}; |
|
|
|
// Detect both own and Ramda placeholder |
|
function isPlaceholder(p) { |
|
return p === _ || (p && p.ramda === 'placeholder'); |
|
} |
|
|
|
function toArray(arg) { |
|
var arr = []; |
|
for (var i = 0; i < arg.length; ++i) { |
|
arr[i] = arg[i]; |
|
} |
|
return arr; |
|
} |
|
|
|
// Modified versions of arity and curryN from Ramda |
|
function ofArity(n, fn) { |
|
if (arguments.length === 1) { |
|
return ofArity.bind(undefined, n); |
|
} |
|
switch (n) { |
|
case 0: |
|
return function () { |
|
return fn.apply(this, arguments); |
|
}; |
|
case 1: |
|
return function (a0) { |
|
void a0; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 2: |
|
return function (a0, a1) { |
|
void a1; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 3: |
|
return function (a0, a1, a2) { |
|
void a2; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 4: |
|
return function (a0, a1, a2, a3) { |
|
void a3; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 5: |
|
return function (a0, a1, a2, a3, a4) { |
|
void a4; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 6: |
|
return function (a0, a1, a2, a3, a4, a5) { |
|
void a5; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 7: |
|
return function (a0, a1, a2, a3, a4, a5, a6) { |
|
void a6; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 8: |
|
return function (a0, a1, a2, a3, a4, a5, a6, a7) { |
|
void a7; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 9: |
|
return function (a0, a1, a2, a3, a4, a5, a6, a7, a8) { |
|
void a8; |
|
return fn.apply(this, arguments); |
|
}; |
|
case 10: |
|
return function (a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { |
|
void a9; |
|
return fn.apply(this, arguments); |
|
}; |
|
default: |
|
throw new Error('First argument to arity must be a non-negative integer no greater than ten'); |
|
} |
|
} |
|
|
|
function curryN(length, fn) { |
|
return ofArity(length, function () { |
|
var n = arguments.length; |
|
var shortfall = length - n; |
|
var idx = n; |
|
while (--idx >= 0) { |
|
if (isPlaceholder(arguments[idx])) { |
|
shortfall += 1; |
|
} |
|
} |
|
if (shortfall <= 0) { |
|
return fn.apply(this, arguments); |
|
} else { |
|
var initialArgs = toArray(arguments); |
|
return curryN(shortfall, function () { |
|
var currentArgs = toArray(arguments); |
|
var combinedArgs = []; |
|
var idx = -1; |
|
while (++idx < n) { |
|
var val = initialArgs[idx]; |
|
combinedArgs[idx] = isPlaceholder(val) ? currentArgs.shift() : val; |
|
} |
|
return fn.apply(this, combinedArgs.concat(currentArgs)); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
|
|
return { |
|
stream: stream, |
|
isStream: isStream, |
|
transduce: transduce, |
|
merge: merge, |
|
reduce: scan, // Legacy |
|
scan: scan, |
|
endsOn: endsOn, |
|
map: curryN(2, map), |
|
curryN: curryN, |
|
_: _, |
|
immediate: immediate, |
|
}; |
|
|
|
})); |