flyd
Version:
The less is more, modular, functional reactive programming library
779 lines (679 loc) • 20.2 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(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 () {
;
function isFunction(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
}
function notUndef(v) {
return v !== undefined;
}
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;
while (s.depsChanged.length > 0) s.depsChanged.shift();
}
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, list, listeners = s.listeners;
for (i = 0; i < listeners.length; ++i) {
list = listeners[i];
if (list.end === s) {
endStream(list);
} else {
list.depsChanged.push(s);
findDeps(list);
}
}
for (i = orderNextIdx; i >= 0; --i) {
if (order[i].depsChanged !== undefined && order[i].depsChanged.length > 0) {
updateStream(order[i]);
}
order[i].queued = false;
}
orderNextIdx = -1;
}
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) list.depsChanged.push(s);
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 = [];
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 s, deps;
var endStream = createDependentStream([], function() { return true; });
if (arguments.length > 1) {
deps = arg.filter(notUndef);
s = createDependentStream(deps, fn);
s.end = endStream;
endStream.listeners.push(s);
var depEndStreams = deps.map(function(d) { return d.end; }).filter(notUndef);
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,
};
}));
},{}],2:[function(require,module,exports){
(function() {
'use strict';
if (self.fetch) {
return
}
function normalizeName(name) {
if (typeof name !== 'string') {
name = name.toString();
}
if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
throw new TypeError('Invalid character in header field name')
}
return name.toLowerCase()
}
function normalizeValue(value) {
if (typeof value !== 'string') {
value = value.toString();
}
return value
}
function Headers(headers) {
this.map = {}
var self = this
if (headers instanceof Headers) {
headers.forEach(function(name, values) {
values.forEach(function(value) {
self.append(name, value)
})
})
} else if (headers) {
Object.getOwnPropertyNames(headers).forEach(function(name) {
self.append(name, headers[name])
})
}
}
Headers.prototype.append = function(name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var list = this.map[name]
if (!list) {
list = []
this.map[name] = list
}
list.push(value)
}
Headers.prototype['delete'] = function(name) {
delete this.map[normalizeName(name)]
}
Headers.prototype.get = function(name) {
var values = this.map[normalizeName(name)]
return values ? values[0] : null
}
Headers.prototype.getAll = function(name) {
return this.map[normalizeName(name)] || []
}
Headers.prototype.has = function(name) {
return this.map.hasOwnProperty(normalizeName(name))
}
Headers.prototype.set = function(name, value) {
this.map[normalizeName(name)] = [normalizeValue(value)]
}
// Instead of iterable for now.
Headers.prototype.forEach = function(callback) {
var self = this
Object.getOwnPropertyNames(this.map).forEach(function(name) {
callback(name, self.map[name])
})
}
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError('Already read'))
}
body.bodyUsed = true
}
function fileReaderReady(reader) {
return new Promise(function(resolve, reject) {
reader.onload = function() {
resolve(reader.result)
}
reader.onerror = function() {
reject(reader.error)
}
})
}
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader()
reader.readAsArrayBuffer(blob)
return fileReaderReady(reader)
}
function readBlobAsText(blob) {
var reader = new FileReader()
reader.readAsText(blob)
return fileReaderReady(reader)
}
var support = {
blob: 'FileReader' in self && 'Blob' in self && (function() {
try {
new Blob();
return true
} catch(e) {
return false
}
})(),
formData: 'FormData' in self
}
function Body() {
this.bodyUsed = false
this._initBody = function(body) {
this._bodyInit = body
if (typeof body === 'string') {
this._bodyText = body
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body
} else if (!body) {
this._bodyText = ''
} else {
throw new Error('unsupported BodyInit type')
}
}
if (support.blob) {
this.blob = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as blob')
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
}
this.arrayBuffer = function() {
return this.blob().then(readBlobAsArrayBuffer)
}
this.text = function() {
var rejected = consumed(this)
if (rejected) {
return rejected
}
if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyFormData) {
throw new Error('could not read FormData body as text')
} else {
return Promise.resolve(this._bodyText)
}
}
} else {
this.text = function() {
var rejected = consumed(this)
return rejected ? rejected : Promise.resolve(this._bodyText)
}
}
if (support.formData) {
this.formData = function() {
return this.text().then(decode)
}
}
this.json = function() {
return this.text().then(JSON.parse)
}
return this
}
// HTTP methods whose capitalization should be normalized
var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
function normalizeMethod(method) {
var upcased = method.toUpperCase()
return (methods.indexOf(upcased) > -1) ? upcased : method
}
function Request(url, options) {
options = options || {}
this.url = url
this.credentials = options.credentials || 'omit'
this.headers = new Headers(options.headers)
this.method = normalizeMethod(options.method || 'GET')
this.mode = options.mode || null
this.referrer = null
if ((this.method === 'GET' || this.method === 'HEAD') && options.body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
}
this._initBody(options.body)
}
function decode(body) {
var form = new FormData()
body.trim().split('&').forEach(function(bytes) {
if (bytes) {
var split = bytes.split('=')
var name = split.shift().replace(/\+/g, ' ')
var value = split.join('=').replace(/\+/g, ' ')
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}
function headers(xhr) {
var head = new Headers()
var pairs = xhr.getAllResponseHeaders().trim().split('\n')
pairs.forEach(function(header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
head.append(key, value)
})
return head
}
Body.call(Request.prototype)
function Response(bodyInit, options) {
if (!options) {
options = {}
}
this._initBody(bodyInit)
this.type = 'default'
this.url = null
this.status = options.status
this.ok = this.status >= 200 && this.status < 300
this.statusText = options.statusText
this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers)
this.url = options.url || ''
}
Body.call(Response.prototype)
self.Headers = Headers;
self.Request = Request;
self.Response = Response;
self.fetch = function(input, init) {
// TODO: Request constructor should accept input, init
var request
if (Request.prototype.isPrototypeOf(input) && !init) {
request = input
} else {
request = new Request(input, init)
}
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest()
function responseURL() {
if ('responseURL' in xhr) {
return xhr.responseURL
}
// Avoid security warnings on getResponseHeader when not allowed by CORS
if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
return xhr.getResponseHeader('X-Request-URL')
}
return;
}
xhr.onload = function() {
var status = (xhr.status === 1223) ? 204 : xhr.status
if (status < 100 || status > 599) {
reject(new TypeError('Network request failed'))
return
}
var options = {
status: status,
statusText: xhr.statusText,
headers: headers(xhr),
url: responseURL()
}
var body = 'response' in xhr ? xhr.response : xhr.responseText;
resolve(new Response(body, options))
}
xhr.onerror = function() {
reject(new TypeError('Network request failed'))
}
xhr.open(request.method, request.url, true)
if (request.credentials === 'include') {
xhr.withCredentials = true
}
if ('responseType' in xhr && support.blob) {
xhr.responseType = 'blob'
}
request.headers.forEach(function(name, values) {
values.forEach(function(value) {
xhr.setRequestHeader(name, value)
})
})
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
self.fetch.polyfill = true
})();
},{}],3:[function(require,module,exports){
var flyd = require('flyd');
require('whatwg-fetch');
document.addEventListener('DOMContentLoaded', function() {
var suggestionsContainerElm = document.querySelector('.suggestions');
var suggestionsElms = suggestionsContainerElm.children;
function update(model, action) { // Take action & model, return updated model
if (action.type === 'refresh') {
return {loaded: [], suggested: []};
} else if (action.type === 'loaded') {
return {loaded: action.data, suggested: action.data.slice(0, 3)};
} else if (action.type === 'remove') {
return {
loaded: model.loaded,
suggested: model.suggested.map(function(sug, i) {
return action.nr === i ? model.loaded[Math.floor(Math.random()*model.loaded.length)]
: sug;
})
};
}
}
function render(model) { // Take model, modify DOM
if (model.suggested.length === 0) {
suggestionsContainerElm.style.visibility = 'hidden';
} else {
suggestionsContainerElm.style.visibility = 'visible';
model.suggested.forEach(function(user, i) {
var suggestionElm = suggestionsElms[i];
var usernameElm = suggestionElm.querySelector('.username');
usernameElm.href = user.html_url;
usernameElm.textContent = user.login;
var imgEl = suggestionElm.querySelector('img');
imgEl.src = '';
imgEl.src = user.avatar_url;
});
}
}
var initialState = {
loaded: [],
suggested: [],
};
// Streams
var action$ = flyd.stream();
var model$ = flyd.scan(update, initialState, action$);
flyd.map(render, model$);
function makeRequest() {
var randomOffset = Math.floor(Math.random()*500);
return fetch('https://api.github.com/users?since=' + randomOffset)
.then(function(res) { return res.json(); })
.then(function(data) { action$({type: 'loaded', data: data}); });
}
makeRequest();
function sendRemoveAction(idx) {
action$({type: 'remove', nr: idx});
}
document.getElementById('refresh').addEventListener('click', makeRequest);
document.getElementById('remove0').addEventListener('click', sendRemoveAction.bind(null, 0));
document.getElementById('remove1').addEventListener('click', sendRemoveAction.bind(null, 1));
document.getElementById('remove2').addEventListener('click', sendRemoveAction.bind(null, 2));
});
},{"flyd":1,"whatwg-fetch":2}]},{},[3]);