<commit_before>var RSVP = require('rsvp'),
    graphite = require('graphite'),
    _ = require('lodash'),
    validator = require('./validator');

/*
 Anatomy of a sensorInfo:
 {
 "id":"2813567",
 "clientName":"Hemma",
 "name":"temperature.firstfloor",
 "lastUpdated":1423447479,
 "ignored":0,
 "editable":1,
 "data":[
 {
 "name":"temp",
 "value":"20.8",
 "scale":"0"
 }],
 "protocol":"fineoffset",
 "sensorId":"66",
 "timezoneoffset":3600,
 "battery":"254"}
 */

module.exports = function (config) {
    var logger = require('log4js-extras')(config).getLogger(__filename),
        client = graphite.createClient(config.graphiteUrl),
        metric = require('./metric')(config.format);

    RSVP.on('error', function (reason) {
        logger.debug('uncaught', reason);
    });

    function logMetric (metric, timestamp) {
        return new RSVP.Promise(function (resolve, reject) {
            client.write(metric, timestamp, function (err) {
                if (!!err) {
                    return reject(err);
                }
                resolve(metric);
            });
        });
    }

    function _logSensorInfo (sensorInfo) {
        return new RSVP.Promise(function (resolve, reject) {
            if (!sensorInfo) {
                return reject(new Error('sensorInfo must not be empty'));
            }
            if (!sensorInfo.name) {
                return reject(new Error('sensorInfo must have a name'));
            }
            if (!validator.isTimestamp(sensorInfo.lastUpdated)) {
                return reject(new Error('lastUpdated is not a timestamp'));
            }
            var promises = sensorInfo.data.map(function (data) {
                if (!data.name) {
                    return reject(new Error('data must have a name'));
                }
                if (!validator.isNumber(data.value)) {
                    return reject(new Error('data must have a numeric value'));
                }
                var m = metric.create(sensorInfo, data),
                    ts = (sensorInfo.lastUpdated * 1000) + sensorInfo.timezoneoffset;
                return logMetric(m, ts);
            });
            return RSVP.all(promises).then(function (metric) {
                resolve(metric);
            }).catch(function (reason) {
                reject(reason);
            });
        });
    }

    /**
     * Log all sensor infos
     * @param sensorInfos list of sensor infos
     * @returns {RSVP.Promise} containing an array of rejected sensor infos
     */
    function logAll (sensorInfos) {
        return RSVP.allSettled(sensorInfos.map(_logSensorInfo)).then(function (results) {
            var fulfilleds = _.pluck(_.where(results, {state: 'fulfilled'}), 'value');
            var rejects = _.pluck(_.where(results, {state: 'rejected'}), 'reason');
            fulfilleds.map(function (fulfilled) {
                logger.info('Logged:', JSON.stringify(fulfilled));
            });
            rejects.map(function (rejected) {
                logger.warn('Rejected:', rejected);
            });
        });
    }

    return {
        logAll: logAll,
        _logSensorInfo: _logSensorInfo
    };
};
<commit_msg>Default timezoneoffset to 0<commit_after>var RSVP = require('rsvp'),
    graphite = require('graphite'),
    _ = require('lodash'),
    validator = require('./validator');

/*
 Anatomy of a sensorInfo:
 {
 "id":"2813567",
 "clientName":"Hemma",
 "name":"temperature.firstfloor",
 "lastUpdated":1423447479,
 "ignored":0,
 "editable":1,
 "data":[
 {
 "name":"temp",
 "value":"20.8",
 "scale":"0"
 }],
 "protocol":"fineoffset",
 "sensorId":"66",
 "timezoneoffset":3600,
 "battery":"254"}
 */

module.exports = function (config) {
    var logger = require('log4js-extras')(config).getLogger(__filename),
        client = graphite.createClient(config.graphiteUrl),
        metric = require('./metric')(config.format);

    RSVP.on('error', function (reason) {
        logger.debug('uncaught', reason);
    });

    function logMetric (metric, timestamp) {
        return new RSVP.Promise(function (resolve, reject) {
            client.write(metric, timestamp, function (err) {
                if (!!err) {
                    return reject(err);
                }
                resolve(metric);
            });
        });
    }

    function _logSensorInfo (sensorInfo) {
        return new RSVP.Promise(function (resolve, reject) {
            if (!sensorInfo) {
                return reject(new Error('sensorInfo must not be empty'));
            }
            if (!sensorInfo.name) {
                return reject(new Error('sensorInfo must have a name'));
            }
            if (!validator.isTimestamp(sensorInfo.lastUpdated)) {
                return reject(new Error('lastUpdated is not a timestamp'));
            }
            var promises = sensorInfo.data.map(function (data) {
                if (!data.name) {
                    return reject(new Error('data must have a name'));
                }
                if (!validator.isNumber(data.value)) {
                    return reject(new Error('data must have a numeric value'));
                }
                var m = metric.create(sensorInfo, data),
                    ts = (sensorInfo.lastUpdated * 1000) + sensorInfo.timezoneoffset || 0;
                return logMetric(m, ts);
            });
            return RSVP.all(promises).then(function (metric) {
                resolve(metric);
            }).catch(function (reason) {
                reject(reason);
            });
        });
    }

    /**
     * Log all sensor infos
     * @param sensorInfos list of sensor infos
     * @returns {RSVP.Promise} containing an array of rejected sensor infos
     */
    function logAll (sensorInfos) {
        return RSVP.allSettled(sensorInfos.map(_logSensorInfo)).then(function (results) {
            var fulfilleds = _.pluck(_.where(results, {state: 'fulfilled'}), 'value');
            var rejects = _.pluck(_.where(results, {state: 'rejected'}), 'reason');
            fulfilleds.map(function (fulfilled) {
                logger.info('Logged:', JSON.stringify(fulfilled));
            });
            rejects.map(function (rejected) {
                logger.warn('Rejected:', rejected);
            });
        });
    }

    return {
        logAll: logAll,
        _logSensorInfo: _logSensorInfo
    };
};
<|endoftext|>


<commit_before>/* core modules */
var fs = require('fs');
var u = require('url');
var p = require('path');
var dns = require('dns');
var http = require('http');
var https = require('https');
var binding = process.binding('net');
/* 3rd party patched module */
var gunzipstream = require('./gunzipstream.js');
/* reads the package.json information */
var pack = JSON.parse(fs.readFileSync(p.resolve(__dirname + '/../package.json')).toString('utf8'));
/**
 * Patches in the http.head() method
 * @param options
 * @param cb
 * @return req
 */
http.head = function (options, cb) {
	options.method = 'HEAD';
	var req = http.request(options, cb);
	req.end();
	return req;
};
/**
 * Patches in the https.head() method
 * @param options
 * @param cb
 * @return req
 */
https.head = function (options, cb) {
	options.method = 'HEAD';
	var req = https.request(options, cb);
	req.end();
	return req;
};
/**
 * Makes all the HTTP request headers to be specified as lower case
 * @param hdr
 * @returns hdr
 */
var normalizeHeaders = function (hdr) {
	for (var name in hdr) {
		var lowName = name.toLowerCase();
		var val = hdr[name];
		delete(hdr[name]);
		hdr[lowName] = val;
	}
	return hdr;
};
/**
 * Returns the absolute integer value of the input. Avoids the NaN crap.
 * @param value
 * @returns value
 */
var absInt = function (value) {
	return Math.abs(parseInt(value) | 0);
};
/**
 * Processes the input parameters for get(), head()
 * @param options
 * @param reqId
 * @returns object
 */
var processInput = function (options, reqId) {
	if (reqId == undefined) {
		reqId = 0;
	}
	if (options.redirects) {
		options.redirects = absInt(options.redirects);
	}
	if ( ! options.redirects) {
		options.redirects = 10;
	}
	var url = options.url.trim(), client = http;
	if ( ! url.match(/^[a-z]{4,5}:\/\//i)) {
		// useful against idiots submitting www.example.com/foo.bar
		// + idiots not doing proper validation
		url = 'http://' + url;
	}
	if ( ! options.headers) {
		options.headers = {};
	}
	var opt = {
		headers: normalizeHeaders(options.headers)
	};
	if ( ! options.nogzip) {
		opt.headers['accept-encoding'] = 'gzip';
	}
	if ( ! opt.headers['user-agent']) {
		opt.headers['user-agent'] = 'http-get/v' + pack.version + ' (https://github.com/SaltwaterC/http-get) ' + 'node.js/' + process.version;
	}
	if ( ! options.proxy) {
		url = u.parse(url);
		url.search = url.search || '';
		url.hash = url.hash || '';
		url.path = url.pathname + url.search + url.hash;
		if ( ! url.port) {
			if (url.protocol == 'https:') {
				url.port = 443;
			} else {
				url.port = 80;
			}
		}
		opt.host = url.hostname;
		opt.port = url.port;
		opt.path = url.path;
		if (url.protocol == 'https:') {
			client = https;
		}
		if (url.auth) {
			opt.headers.authorization = new Buffer(url.auth).toString('base64');
		}
	} else {
		opt.host = options.proxy.host;
		opt.port = options.proxy.port;
		opt.path = url;
		if (options.proxy.https) {
			client = https;
		}
	}
	return {
		options: options,
		opt: opt,
		reqId: reqId,
		client: client,
		url: url
	};
};
/**
 * Prepares the redirect target
 * @param url
 * @param location
 * @returns string
 */
var prepareRedirectUrl = function (url, location) {
	var original = u.parse(url);
	var location = u.parse(location);
	if ( ! location.protocol) {
		location.protocol = original.protocol;
	}
	if ( ! location.host) {
		location.host = original.host;
	}
	return u.format(location);
};
/**
 * Sets the http.Agent.defaultMaxSockets property
 * @param value
 */
var setMaxSockets = function (value) {
	value = absInt(value);
	if ( ! value) { // fallback to the default
		value = 5;
	}
	http.Agent.defaultMaxSockets = value;
};
exports.setMaxSockets = setMaxSockets;
/**
 * Avoids the node.js bug #1202
 * @param opt
 * @param clientCall
 * @param processRequest
 * @param cb
 */
var avoid1202 = function (opt, clientCall, processRequest, cb) {
	var addrType = binding.isIP(opt.host);
	if (addrType == 4 || addrType == 6) {
		// the host is an IP address, skipping the DNS resolving
		// no IPv6 for the moment due to lack of support in url.parse()
		processRequest(clientCall(opt));
	} else {
		// apply the DNS hack
		dns.lookup(opt.host, function (err, address) {
			if (err) {
				cb(err);
			} else {
				// patch the host header to comply with HTTP/1.1
				opt.headers.host = opt.host;
				// pass the host as resolved address in order to avoid redundant DNS resolving
				opt.host = address;
				processRequest(clientCall(opt));
			}
		});
	}
};
/**
 * The http.get() method
 * @param options
 * @param file
 * @param cb
 * @param reqId
 */
var get = function (options, file, cb, reqId) {
	if (typeof file == 'function') {
		cb = file;
		file = null;
		if (options.maxbody) {
			options.maxbody = absInt(options.maxbody);
			if ( ! options.maxbody) {
				cb(new Error('Invalid options.maxbody specification.'));
				return;
			}
		}
	} else {
		file = file.trim();
		file = p.resolve(file);
	}
	if ( ! options.url) {
		cb(new Error('The options object requires an input URL value.'));
		return;
	}
	var input = processInput(options, reqId);
	var opt = input.opt;
	var client = input.client;
	var url = input.url;
	options = input.options;
	reqId = input.reqId;
	var processRequest = function (req) {
		req.on('response', function (res) {
			switch (res.statusCode) {
				case 200: // success
					var aborted = false;
					if (file) {
						var ws = fs.createWriteStream(file);
						ws.on('error', function (err) {
							aborted = true;
							req.abort();
							cb(err);
						});
					} else {
						var buf = '';
					}
					var flushData = function (chunk) {
						if (file) {
							try {
								ws.write(chunk);
							} catch (e) {} // handled by the error listener
						} else {
							if ( ! aborted) {
								buf += chunk;
								if (options.maxbody) {
									if (buf.length > options.maxbody) {
										aborted = true;
										req.abort();
										var err = new Error('Large body detected.');
										err.code = 200;
										err.headers = res.headers;
										cb(err);
									}
								}
							}
						}
					};
					var endRequest = function () {
						if ( ! aborted) {
							var ret = {
								code: 200,
								headers: res.headers
							};
							if (file) {
								ws.end();
								fs.open(file, 'r', function (err, fd) {
									if ( ! aborted) {
										if (err) {
											err.code = res.statusCode;
											err.headers = res.headers;
											cb(err);
										} else {
											fs.fsync(fd, function () {
												fs.close(fd, function () {
													ret.file = file;
													if (reqId > 0) {
														ret.url = options.url;
													}
													cb(null, ret);
												});
											});
										}
									} else {
										if ( ! err) {
											fs.close(fd);
										}
									}
								});
							} else {
								ret.buffer = buf;
								if (reqId > 0) {
									ret.url = options.url;
								}
								cb(null, ret);
							}
						}
					};
					var gzEncoding = false;
					if (res.headers['content-encoding'] == 'gzip') {
						gzEncoding = true;
						var gzEnd = false;
						var gunzip = gunzipstream.wrap(res, {encoding: null});
						gunzip.on('data', function (chunk) {
							if ( ! aborted) {
								flushData(chunk);
							}
						});
						gunzip.on('end', function () {
							if ( ! gzEnd) {
								gzEnd = true;
								endRequest();
							}
						});
						gunzip.on('close', function () {
							if ( ! gzEnd) {
								gzEnd = true;
								endRequest();
							}
						});
						gunzip.on('error', function (err) {
							if ( ! aborted) {
								aborted = true;
								req.abort();
								delete(options.headers['accept-encoding']);
								options.nogzip = true;
								if ( ! file) {
									file = cb;
								}
								get(options, file, cb, reqId);
							}
						});
					}
					res.on('data', function (chunk) {
						if ( ! gzEncoding && ! aborted) {
							flushData(chunk);
						}
					});
					res.on('end', function () {
						if ( ! gzEncoding && ! aborted) {
							endRequest();
						}
					});
					res.on('close', function () {
						var err = new Error('The server prematurely closed the connection.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					});
				break;
				case 300: // redirect
				case 301:
				case 302:
				case 303:
				case 305:
				case 307:
					if ( ! res.headers.location) {
						var err = new Error('Redirect response without location header.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					} else {
						options.url = prepareRedirectUrl(url, res.headers.location);
						if (reqId < options.redirects) {
							reqId++;
							if ( ! file) {
								file = cb;
							}
							get(options, file, cb, reqId);
						} else {
							var err = new Error('Redirect loop detected after ' + Number(reqId) + ' requests.');
							err.code = res.statusCode;
							err.headers = res.headers;
							cb(err);
						}
					}
				break;
				case 304:
					var ret = {
						code: 304,
						headers: res.headers
					};
					if (reqId > 0) {
						ret.url = options.url;
					}
					cb(null, ret);
				break;
				default: // error
					var err = new Error('HTTP Error: ' + Number(res.statusCode));
					err.code = res.statusCode;
					err.headers = res.headers;
					cb(err);
				break;
			}
		});
		req.on('error', function (err) {
			cb(err);
		});
		req.end();
	};
	avoid1202(opt, client.get, processRequest, cb);
};
exports.get = get;
/**
 * The http.head() method
 * @param options
 * @param cb
 * @param reqId
 */
var head = function (options, cb, reqId) {
	if ( ! options.url) {
		cb(new Error('The options object requires an input URL value.'));
		return;
	}
	var input = processInput(options, reqId);
	var opt = input.opt;
	var client = input.client;
	var url = input.url;
	options = input.options;
	reqId = input.reqId;
	var processRequest = function (req) {
		req.on('response', function (res) {
			switch (res.statusCode) {
				case 200:
				case 304:
					var ret = {
						code: res.statusCode,
						headers: res.headers
					};
					if (reqId > 0) {
						ret.url = options.url;
					}
					cb(null, ret);
				break;
				case 300: // redirect
				case 301:
				case 302:
				case 303:
				case 305:
				case 307:
					if ( ! res.headers.location) {
						var err = new Error('Redirect response without location header.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					} else {
						options.url = prepareRedirectUrl(url, res.headers.location);
						if (reqId < options.redirects) {
							reqId++;
							head(options, cb, reqId);
						} else {
							var err = new Error('Redirect loop detected after ' + Number(reqId) + ' requests.');
							err.code = res.statusCode;
							err.headers = res.headers;
							cb(err);
						}
					}
				break;
				default: // error
					var err = new Error('HTTP Error: ' + Number(res.statusCode));
					err.code = res.statusCode;
					err.headers = res.headers;
					cb(err);
				break;
			}
		});
		req.on('error', function (err) {
			cb(err);
		});
		req.end();
	};
	avoid1202(opt, client.head, processRequest, cb);
};
exports.head = head;
<commit_msg>Removes the hack for working around the node.js issue 1202 since the it's fixed by the upstream release 0.4.11.<commit_after>/* core modules */
var fs = require('fs');
var u = require('url');
var p = require('path');
var dns = require('dns');
var http = require('http');
var https = require('https');
var binding = process.binding('net');
/* 3rd party patched module */
var gunzipstream = require('./gunzipstream.js');
/* reads the package.json information */
var pack = JSON.parse(fs.readFileSync(p.resolve(__dirname + '/../package.json')).toString('utf8'));
/**
 * Patches in the http.head() method
 * @param options
 * @param cb
 * @return req
 */
http.head = function (options, cb) {
	options.method = 'HEAD';
	var req = http.request(options, cb);
	req.end();
	return req;
};
/**
 * Patches in the https.head() method
 * @param options
 * @param cb
 * @return req
 */
https.head = function (options, cb) {
	options.method = 'HEAD';
	var req = https.request(options, cb);
	req.end();
	return req;
};
/**
 * Makes all the HTTP request headers to be specified as lower case
 * @param hdr
 * @returns hdr
 */
var normalizeHeaders = function (hdr) {
	for (var name in hdr) {
		var lowName = name.toLowerCase();
		var val = hdr[name];
		delete(hdr[name]);
		hdr[lowName] = val;
	}
	return hdr;
};
/**
 * Returns the absolute integer value of the input. Avoids the NaN crap.
 * @param value
 * @returns value
 */
var absInt = function (value) {
	return Math.abs(parseInt(value) | 0);
};
/**
 * Processes the input parameters for get(), head()
 * @param options
 * @param reqId
 * @returns object
 */
var processInput = function (options, reqId) {
	if (reqId == undefined) {
		reqId = 0;
	}
	if (options.redirects) {
		options.redirects = absInt(options.redirects);
	}
	if ( ! options.redirects) {
		options.redirects = 10;
	}
	var url = options.url.trim(), client = http;
	if ( ! url.match(/^[a-z]{4,5}:\/\//i)) {
		// useful against idiots submitting www.example.com/foo.bar
		// + idiots not doing proper validation
		url = 'http://' + url;
	}
	if ( ! options.headers) {
		options.headers = {};
	}
	var opt = {
		headers: normalizeHeaders(options.headers)
	};
	if ( ! options.nogzip) {
		opt.headers['accept-encoding'] = 'gzip';
	}
	if ( ! opt.headers['user-agent']) {
		opt.headers['user-agent'] = 'http-get/v' + pack.version + ' (https://github.com/SaltwaterC/http-get) ' + 'node.js/' + process.version;
	}
	if ( ! options.proxy) {
		url = u.parse(url);
		url.search = url.search || '';
		url.hash = url.hash || '';
		url.path = url.pathname + url.search + url.hash;
		if ( ! url.port) {
			if (url.protocol == 'https:') {
				url.port = 443;
			} else {
				url.port = 80;
			}
		}
		opt.host = url.hostname;
		opt.port = url.port;
		opt.path = url.path;
		if (url.protocol == 'https:') {
			client = https;
		}
		if (url.auth) {
			opt.headers.authorization = new Buffer(url.auth).toString('base64');
		}
	} else {
		opt.host = options.proxy.host;
		opt.port = options.proxy.port;
		opt.path = url;
		if (options.proxy.https) {
			client = https;
		}
	}
	return {
		options: options,
		opt: opt,
		reqId: reqId,
		client: client,
		url: url
	};
};
/**
 * Prepares the redirect target
 * @param url
 * @param location
 * @returns string
 */
var prepareRedirectUrl = function (url, location) {
	var original = u.parse(url);
	var location = u.parse(location);
	if ( ! location.protocol) {
		location.protocol = original.protocol;
	}
	if ( ! location.host) {
		location.host = original.host;
	}
	return u.format(location);
};
/**
 * Sets the http.Agent.defaultMaxSockets property
 * @param value
 */
var setMaxSockets = function (value) {
	value = absInt(value);
	if ( ! value) { // fallback to the default
		value = 5;
	}
	http.Agent.defaultMaxSockets = value;
};
exports.setMaxSockets = setMaxSockets;
/**
 * The http.get() method
 * @param options
 * @param file
 * @param cb
 * @param reqId
 */
var get = function (options, file, cb, reqId) {
	if (typeof file == 'function') {
		cb = file;
		file = null;
		if (options.maxbody) {
			options.maxbody = absInt(options.maxbody);
			if ( ! options.maxbody) {
				cb(new Error('Invalid options.maxbody specification.'));
				return;
			}
		}
	} else {
		file = file.trim();
		file = p.resolve(file);
	}
	if ( ! options.url) {
		cb(new Error('The options object requires an input URL value.'));
		return;
	}
	var input = processInput(options, reqId);
	var opt = input.opt;
	var client = input.client;
	var url = input.url;
	options = input.options;
	reqId = input.reqId;
	var processRequest = function (req) {
		req.on('response', function (res) {
			switch (res.statusCode) {
				case 200: // success
					var aborted = false;
					if (file) {
						var ws = fs.createWriteStream(file);
						ws.on('error', function (err) {
							aborted = true;
							req.abort();
							cb(err);
						});
					} else {
						var buf = '';
					}
					var flushData = function (chunk) {
						if (file) {
							try {
								ws.write(chunk);
							} catch (e) {} // handled by the error listener
						} else {
							if ( ! aborted) {
								buf += chunk;
								if (options.maxbody) {
									if (buf.length > options.maxbody) {
										aborted = true;
										req.abort();
										var err = new Error('Large body detected.');
										err.code = 200;
										err.headers = res.headers;
										cb(err);
									}
								}
							}
						}
					};
					var endRequest = function () {
						if ( ! aborted) {
							var ret = {
								code: 200,
								headers: res.headers
							};
							if (file) {
								ws.end();
								fs.open(file, 'r', function (err, fd) {
									if ( ! aborted) {
										if (err) {
											err.code = res.statusCode;
											err.headers = res.headers;
											cb(err);
										} else {
											fs.fsync(fd, function () {
												fs.close(fd, function () {
													ret.file = file;
													if (reqId > 0) {
														ret.url = options.url;
													}
													cb(null, ret);
												});
											});
										}
									} else {
										if ( ! err) {
											fs.close(fd);
										}
									}
								});
							} else {
								ret.buffer = buf;
								if (reqId > 0) {
									ret.url = options.url;
								}
								cb(null, ret);
							}
						}
					};
					var gzEncoding = false;
					if (res.headers['content-encoding'] == 'gzip') {
						gzEncoding = true;
						var gzEnd = false;
						var gunzip = gunzipstream.wrap(res, {encoding: null});
						gunzip.on('data', function (chunk) {
							if ( ! aborted) {
								flushData(chunk);
							}
						});
						gunzip.on('end', function () {
							if ( ! gzEnd) {
								gzEnd = true;
								endRequest();
							}
						});
						gunzip.on('close', function () {
							if ( ! gzEnd) {
								gzEnd = true;
								endRequest();
							}
						});
						gunzip.on('error', function (err) {
							if ( ! aborted) {
								aborted = true;
								req.abort();
								delete(options.headers['accept-encoding']);
								options.nogzip = true;
								if ( ! file) {
									file = cb;
								}
								get(options, file, cb, reqId);
							}
						});
					}
					res.on('data', function (chunk) {
						if ( ! gzEncoding && ! aborted) {
							flushData(chunk);
						}
					});
					res.on('end', function () {
						if ( ! gzEncoding && ! aborted) {
							endRequest();
						}
					});
					res.on('close', function () {
						var err = new Error('The server prematurely closed the connection.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					});
				break;
				case 300: // redirect
				case 301:
				case 302:
				case 303:
				case 305:
				case 307:
					if ( ! res.headers.location) {
						var err = new Error('Redirect response without location header.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					} else {
						options.url = prepareRedirectUrl(url, res.headers.location);
						if (reqId < options.redirects) {
							reqId++;
							if ( ! file) {
								file = cb;
							}
							get(options, file, cb, reqId);
						} else {
							var err = new Error('Redirect loop detected after ' + Number(reqId) + ' requests.');
							err.code = res.statusCode;
							err.headers = res.headers;
							cb(err);
						}
					}
				break;
				case 304:
					var ret = {
						code: 304,
						headers: res.headers
					};
					if (reqId > 0) {
						ret.url = options.url;
					}
					cb(null, ret);
				break;
				default: // error
					var err = new Error('HTTP Error: ' + Number(res.statusCode));
					err.code = res.statusCode;
					err.headers = res.headers;
					cb(err);
				break;
			}
		});
		req.on('error', function (err) {
			cb(err);
		});
		req.end();
	};
	processRequest(client.get(opt));
};
exports.get = get;
/**
 * The http.head() method
 * @param options
 * @param cb
 * @param reqId
 */
var head = function (options, cb, reqId) {
	if ( ! options.url) {
		cb(new Error('The options object requires an input URL value.'));
		return;
	}
	var input = processInput(options, reqId);
	var opt = input.opt;
	var client = input.client;
	var url = input.url;
	options = input.options;
	reqId = input.reqId;
	var processRequest = function (req) {
		req.on('response', function (res) {
			switch (res.statusCode) {
				case 200:
				case 304:
					var ret = {
						code: res.statusCode,
						headers: res.headers
					};
					if (reqId > 0) {
						ret.url = options.url;
					}
					cb(null, ret);
				break;
				case 300: // redirect
				case 301:
				case 302:
				case 303:
				case 305:
				case 307:
					if ( ! res.headers.location) {
						var err = new Error('Redirect response without location header.');
						err.code = res.statusCode;
						err.headers = res.headers;
						cb(err);
					} else {
						options.url = prepareRedirectUrl(url, res.headers.location);
						if (reqId < options.redirects) {
							reqId++;
							head(options, cb, reqId);
						} else {
							var err = new Error('Redirect loop detected after ' + Number(reqId) + ' requests.');
							err.code = res.statusCode;
							err.headers = res.headers;
							cb(err);
						}
					}
				break;
				default: // error
					var err = new Error('HTTP Error: ' + Number(res.statusCode));
					err.code = res.statusCode;
					err.headers = res.headers;
					cb(err);
				break;
			}
		});
		req.on('error', function (err) {
			cb(err);
		});
		req.end();
	};
	processRequest(client.head(opt));
};
exports.head = head;
<|endoftext|>


<commit_before>const React = require('react');
const Radium = require('radium');

const Spin = require('./Spin');

const StyleConstants = require('../constants/Style');

const Icon = require('../components/Icon');

const Button = React.createClass({
  propTypes: {
    actionText: React.PropTypes.string,
    icon: React.PropTypes.string,
    isActive: React.PropTypes.bool,
    primaryColor: React.PropTypes.string,
    style: React.PropTypes.object,
    type: React.PropTypes.oneOf([
      'base',
      'disabled',
      'neutral',
      'primary',
      'primaryOutline',
      'secondary'
    ])
  },

  getDefaultProps () {
    return {
      isActive: false,
      primaryColor: StyleConstants.Colors.PRIMARY,
      type: 'primary'
    };
  },

  render () {
    const styles = this.styles();

    return (

        {this.props.icon && !this.props.isActive ? <Icon size={20} style={styles.icon} type={this.props.icon} /> : null}
        {this.props.isActive ? (

            <Spin direction='counterclockwise'>
              <Icon size={20} style={styles.spinner} type='spinner' />
            </Spin>
              {this.props.actionText ?
 {this.props.actionText}
 : null }

        ) : this.props.children}

    );
  },

  styles () {
    return {
      component: Object.assign({
        borderRadius: 2,
        borderStyle: 'solid',
        borderWidth: 1,
        borderColor: 'transparent',
        display: 'inline-block',
        padding: '7px 14px',
        textAlign: 'center',
        fontSize: StyleConstants.FontSizes.MEDIUM,
        fontFamily: StyleConstants.Fonts.SEMIBOLD,
        cursor: 'pointer',
        transition: 'all .2s ease-in',
        minWidth: 16,
        height: 30
      }, this.props.style),
      primary: {
        backgroundColor: this.props.primaryColor,
        borderColor: this.props.primaryColor,
        color: StyleConstants.Colors.WHITE,
        fill: StyleConstants.Colors.WHITE,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -15),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -15),
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          transition: 'all .2s ease-in'
        }
      },
      primaryOutline: {
        backgroundColor: 'transparent',
        borderColor: this.props.primaryColor,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: this.props.primaryColor,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      secondary: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.ASH,
        color: StyleConstants.Colors.ASH,
        fill: StyleConstants.Colors.ASH,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.Colors.ASH,
          borderColor: StyleConstants.Colors.ASH,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -30),
          borderColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -30),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      base: {
        backgroundColor: 'transparent',
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',
        borderColor: 'transparent',
        borderRadius: 2,
        borderWidth: 1,
        ':hover': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -8),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -8),
          transition: 'all .2s ease-in',
          borderColor: StyleConstants.Colors.FOG
        },
        ':active': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -16),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in',
          backgroundColor: StyleConstants.Colors.PORCELAIN
        }
      },
      neutral: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.FOG,
        borderRadius: 2,
        borderWidth: 1,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        ':hover': {
          backgroundColor: StyleConstants.Colors.PORCELAIN
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.PORCELAIN, -15)
        }
      },
      disabled: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.FOG,
        color: StyleConstants.Colors.FOG,
        fill: StyleConstants.Colors.FOG
      },
      icon: {
        marginBottom: -3,
        marginLeft: -5,
        marginRight: this.props.children ? 5 : -5,
        marginTop: -4,
        verticalAlign: 'middle'
      },
      spinner: {
        marginBottom: -3,
        marginLeft: -5,
        marginRight: -5,
        marginTop: -4
      },
      actionText: {
        display: 'inline-block',
        paddingLeft: 10
      }
    };
  }
});

module.exports = Radium(Button);
<commit_msg>tweak text margin in button<commit_after>const React = require('react');
const Radium = require('radium');

const Spin = require('./Spin');

const StyleConstants = require('../constants/Style');

const Icon = require('../components/Icon');

const Button = React.createClass({
  propTypes: {
    actionText: React.PropTypes.string,
    icon: React.PropTypes.string,
    isActive: React.PropTypes.bool,
    primaryColor: React.PropTypes.string,
    style: React.PropTypes.object,
    type: React.PropTypes.oneOf([
      'base',
      'disabled',
      'neutral',
      'primary',
      'primaryOutline',
      'secondary'
    ])
  },

  getDefaultProps () {
    return {
      isActive: false,
      primaryColor: StyleConstants.Colors.PRIMARY,
      type: 'primary'
    };
  },

  render () {
    const styles = this.styles();

    return (

        {this.props.icon && !this.props.isActive ? <Icon size={20} style={styles.icon} type={this.props.icon} /> : null}
        {this.props.isActive ? (

            <Spin direction='counterclockwise'>
              <Icon size={20} style={styles.spinner} type='spinner' />
            </Spin>
              {this.props.actionText ?
 {this.props.actionText}
 : null }

        ) : this.props.children}

    );
  },

  styles () {
    return {
      component: Object.assign({
        borderRadius: 2,
        borderStyle: 'solid',
        borderWidth: 1,
        borderColor: 'transparent',
        display: 'inline-block',
        padding: '7px 14px',
        textAlign: 'center',
        fontSize: StyleConstants.FontSizes.MEDIUM,
        fontFamily: StyleConstants.Fonts.SEMIBOLD,
        cursor: 'pointer',
        transition: 'all .2s ease-in',
        minWidth: 16,
        height: 30,
        lineHeight: '1.2em'
      }, this.props.style),
      primary: {
        backgroundColor: this.props.primaryColor,
        borderColor: this.props.primaryColor,
        color: StyleConstants.Colors.WHITE,
        fill: StyleConstants.Colors.WHITE,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -15),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -15),
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          transition: 'all .2s ease-in'
        }
      },
      primaryOutline: {
        backgroundColor: 'transparent',
        borderColor: this.props.primaryColor,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: this.props.primaryColor,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      secondary: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.ASH,
        color: StyleConstants.Colors.ASH,
        fill: StyleConstants.Colors.ASH,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.Colors.ASH,
          borderColor: StyleConstants.Colors.ASH,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -30),
          borderColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -30),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      base: {
        backgroundColor: 'transparent',
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',
        borderColor: 'transparent',
        borderRadius: 2,
        borderWidth: 1,
        ':hover': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -8),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -8),
          transition: 'all .2s ease-in',
          borderColor: StyleConstants.Colors.FOG
        },
        ':active': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -16),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in',
          backgroundColor: StyleConstants.Colors.PORCELAIN
        }
      },
      neutral: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.FOG,
        borderRadius: 2,
        borderWidth: 1,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        ':hover': {
          backgroundColor: StyleConstants.Colors.PORCELAIN
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.PORCELAIN, -15)
        }
      },
      disabled: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.FOG,
        color: StyleConstants.Colors.FOG,
        fill: StyleConstants.Colors.FOG
      },
      icon: {
        marginBottom: -3,
        marginLeft: -5,
        marginRight: this.props.children ? 5 : -5,
        marginTop: -3,
        verticalAlign: 'middle'
      },
      spinner: {
        marginBottom: -3,
        marginLeft: -5,
        marginRight: -5,
        marginTop: -3
      },
      actionText: {
        display: 'inline-block',
        paddingLeft: 10
      }
    };
  }
});

module.exports = Radium(Button);
<|endoftext|>


<commit_before>const React = require('react');
const Radium = require('radium');

const Spin = require('./Spin');

const StyleConstants = require('../constants/Style');

const Icon = require('../components/Icon');

const Button = React.createClass({
  propTypes: {
    actionText: React.PropTypes.string,
    icon: React.PropTypes.string,
    isActive: React.PropTypes.bool,
    primaryColor: React.PropTypes.string,
    type: React.PropTypes.oneOf([
      'base',
      'disabled',
      'neutral',
      'primary',
      'primaryOutline',
      'secondary'
    ])
  },

  getDefaultProps () {
    return {
      isActive: false,
      primaryColor: StyleConstants.Colors.PRIMARY,
      type: 'primary'
    };
  },

  _renderButtonContent () {
    const styles = this.styles();

    if (this.props.isActive) {
      return (

        <Spin direction='counterclockwise'>
            <Icon size='20' style={[styles.icon, styles.spinner]} type='spinner' />
        </Spin>
          {this.props.actionText ?
 {this.props.actionText}
 : null }

      );
    } else {
      return this.props.children;
    }
  },

  render () {
    const styles = this.styles();

    return (

        {this.props.icon && !this.props.isActive ? <Icon size={20} style={styles.icon} type={this.props.icon} /> : null}
        {this._renderButtonContent()}

    );
  },

  styles () {
    return {
      component: {
        borderRadius: 2,
        borderStyle: 'solid',
        borderWidth: 1,
        borderColor: 'transparent',
        display: 'inline-block',
        padding: '7px 14px',
        textAlign: 'center',
        fontSize: StyleConstants.FontSizes.MEDIUM,
        fontFamily: StyleConstants.Fonts.SEMIBOLD,
        cursor: 'pointer',
        transition: 'all .2s ease-in',
        minWidth: 50
      },
      primary: {
        backgroundColor: this.props.primaryColor,
        borderColor: this.props.primaryColor,
        color: StyleConstants.Colors.WHITE,
        fill: StyleConstants.Colors.WHITE,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in'
        }
      },
      primaryOutline: {
        backgroundColor: 'transparent',
        borderColor: this.props.primaryColor,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: this.props.primaryColor,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      secondary: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.ASH,
        color: StyleConstants.Colors.ASH,
        fill: StyleConstants.Colors.ASH,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.Colors.ASH,
          borderColor: StyleConstants.Colors.ASH,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -10),
          borderColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -10),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      base: {
        backgroundColor: 'transparent',
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -8),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -8),
          transition: 'all .2s ease-in'
        },
        ':active': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -16),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in'
        }
      },
      disabled: {
        backgroundColor: StyleConstants.Colors.PORCELAIN,
        borderColor: StyleConstants.Colors.PORCELAIN,
        color: StyleConstants.Colors.FOG,
        fill: StyleConstants.Colors.FOG
      },
      icon: {
        marginTop: -6,
        marginBottom: -5,
        marginLeft: -5,
        marginRight: this.props.children ? 5 : -5
      },
      spinner: {
        verticalAlign: 'initial',
        marginRight: -5,
        padding: !this.props.actionText && this.props.children ? 3 : 0
      },
      actionText: {
        display: 'inline-block',
        paddingLeft: 10
      }
    };
  }
});

module.exports = Radium(Button);
<commit_msg>consolidates render functions<commit_after>const React = require('react');
const Radium = require('radium');

const Spin = require('./Spin');

const StyleConstants = require('../constants/Style');

const Icon = require('../components/Icon');

const Button = React.createClass({
  propTypes: {
    actionText: React.PropTypes.string,
    icon: React.PropTypes.string,
    isActive: React.PropTypes.bool,
    primaryColor: React.PropTypes.string,
    type: React.PropTypes.oneOf([
      'base',
      'disabled',
      'neutral',
      'primary',
      'primaryOutline',
      'secondary'
    ])
  },

  getDefaultProps () {
    return {
      isActive: false,
      primaryColor: StyleConstants.Colors.PRIMARY,
      type: 'primary'
    };
  },

  render () {
    const styles = this.styles();

    return (

        {this.props.icon && !this.props.isActive ? <Icon size={20} style={styles.icon} type={this.props.icon} /> : null}
        {this.props.isActive ? (

            <Spin direction='counterclockwise'>
                <Icon size='20' style={[styles.icon, styles.spinner]} type='spinner' />
            </Spin>
              {this.props.actionText ?
 {this.props.actionText}
 : null }

        ) : this.props.children}

    );
  },

  styles () {
    return {
      component: {
        borderRadius: 2,
        borderStyle: 'solid',
        borderWidth: 1,
        borderColor: 'transparent',
        display: 'inline-block',
        padding: '7px 14px',
        textAlign: 'center',
        fontSize: StyleConstants.FontSizes.MEDIUM,
        fontFamily: StyleConstants.Fonts.SEMIBOLD,
        cursor: 'pointer',
        transition: 'all .2s ease-in',
        minWidth: 50
      },
      primary: {
        backgroundColor: this.props.primaryColor,
        borderColor: this.props.primaryColor,
        color: StyleConstants.Colors.WHITE,
        fill: StyleConstants.Colors.WHITE,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -30),
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in'
        }
      },
      primaryOutline: {
        backgroundColor: 'transparent',
        borderColor: this.props.primaryColor,
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: this.props.primaryColor,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          borderColor: StyleConstants.adjustColor(this.props.primaryColor, -16),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      secondary: {
        backgroundColor: 'transparent',
        borderColor: StyleConstants.Colors.ASH,
        color: StyleConstants.Colors.ASH,
        fill: StyleConstants.Colors.ASH,
        transition: 'all .2s ease-in',

        ':hover': {
          backgroundColor: StyleConstants.Colors.ASH,
          borderColor: StyleConstants.Colors.ASH,
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        },
        ':active': {
          backgroundColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -10),
          borderColor: StyleConstants.adjustColor(StyleConstants.Colors.ASH, -10),
          color: StyleConstants.Colors.WHITE,
          fill: StyleConstants.Colors.WHITE,
          transition: 'all .2s ease-in'
        }
      },
      base: {
        backgroundColor: 'transparent',
        color: this.props.primaryColor,
        fill: this.props.primaryColor,
        transition: 'all .2s ease-in',

        ':hover': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -8),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -8),
          transition: 'all .2s ease-in'
        },
        ':active': {
          color: StyleConstants.adjustColor(this.props.primaryColor, -16),
          fill: StyleConstants.adjustColor(this.props.primaryColor, -16),
          transition: 'all .2s ease-in'
        }
      },
      disabled: {
        backgroundColor: StyleConstants.Colors.PORCELAIN,
        borderColor: StyleConstants.Colors.PORCELAIN,
        color: StyleConstants.Colors.FOG,
        fill: StyleConstants.Colors.FOG
      },
      icon: {
        marginTop: -6,
        marginBottom: -5,
        marginLeft: -5,
        marginRight: this.props.children ? 5 : -5
      },
      spinner: {
        verticalAlign: 'initial',
        marginRight: -5,
        padding: !this.props.actionText && this.props.children ? 3 : 0
      },
      actionText: {
        display: 'inline-block',
        paddingLeft: 10
      }
    };
  }
});

module.exports = Radium(Button);
<|endoftext|>


<commit_before>import React from 'react'
import _map from 'lodash.map'
import FitText from '../vendor/react-fittext'

class Footer extends React.Component {
  render () {
    const self = this
    const data = self.props.data

    var typefaceList = _map(Object.keys(data.fonts), function (index) {
      var font = data.fonts[index]
      if (font.published !== false && font.designer) {
        return (

                <FitText compressor={0.33} minFontSize={24}>

あア

                </FitText>

                { font.name.ja } { font.name.en !== font.name.ja ? font.name.en : '' }
                { font.designer.name.ja } { font.designer.name.en !== font.designer.name.ja ? font.designer.name.en : '' }

        )
      }
    })

    return (

ぜひ定期的にチェックしてください

このページでは、早期アクセスに追加された新しいフォントのサンプルをご利用いただけます。また、Google での試験運用や技術についてお知らせすることもあります。

Check back often!

We’ll update this page with samples as new fonts are added to Early Access, and provide information about our own experiments and technology when possible.

About

Contribute

@{ data.social.twitter } on Twitter

@{ data.social.github } on GitHub

Featured Typefaces

{ typefaceList }

Made by Friends of Google Fonts

                    { data.authors['marikotakagi'].name }

{ data.authors['marikotakagi'].credit }

                    { data.authors['kennethormandy'].name }

{ data.authors['kennethormandy'].credit }

    )
  }
}

export default Footer
<commit_msg>Updates footer credit columns<commit_after>import React from 'react'
import _map from 'lodash.map'
import FitText from '../vendor/react-fittext'

class Footer extends React.Component {
  render () {
    const self = this
    const data = self.props.data

    var typefaceList = _map(Object.keys(data.fonts), function (index) {
      var font = data.fonts[index]
      if (font.published !== false && font.designer) {
        return (

                <FitText compressor={0.33} minFontSize={24}>

あア

                </FitText>

                { font.name.ja } { font.name.en !== font.name.ja ? font.name.en : '' }
                { font.designer.name.ja } { font.designer.name.en !== font.designer.name.ja ? font.designer.name.en : '' }

        )
      }
    })

    return (

ぜひ定期的にチェックしてください

このページでは、早期アクセスに追加された新しいフォントのサンプルをご利用いただけます。また、Google での試験運用や技術についてお知らせすることもあります。

Check back often!

We’ll update this page with samples as new fonts are added to Early Access, and provide information about our own experiments and technology when possible.

About

Contribute

@{ data.social.twitter } on Twitter

@{ data.social.github } on GitHub

Featured Typefaces

{ typefaceList }

Made by Friends of Google Fonts

                    { data.authors['marikotakagi'].name }

{ data.authors['marikotakagi'].credit }

                    { data.authors['kennethormandy'].name }

{ data.authors['kennethormandy'].credit }

    )
  }
}

export default Footer
<|endoftext|>


<commit_before>import React from 'react'
import { Subhead, Container, Flex, Box, Text, Row, Column } from 'rebass'
import { Link } from 'react-static'
import Icon from './Icon'
import { mx, geo, grays } from '../theme'

const Base = Box.extend.attrs({ is: 'footer', py: 5, px: 2 })`
  ${geo(grays.snow)};
`

const LeftCol = Column.extend.attrs({ w: [1, 0.4], mb: 2 })`
  ${mx[1]} {
    text-align: right;
  }
`
const RightCol = Column.extend.attrs({ w: [1, 0.6] })`
  text-align: left;
`

const Service = ({ href, icon, fill = 'muted', size = 32, ...props }) => (
  <Box is="a" target="_blank" href={href} mx={2} {...props}>
    <Icon name={icon} size={size} fill={fill} />
  </Box>
)

const Footer = () => (
  <Base>
    <Container>
      <Row w={1} wrap>
        <LeftCol>
          <Subhead>Join the Club</Subhead>
        </LeftCol>
        <RightCol>
          <Flex align="center" mx={-2}>
            <Service href="/slack_invite" icon="slack" />
            <Service href="https://twitter.com/starthackclub" icon="twitter" />
            <Service href="https://github.com/hackclub" icon="github" />
            <Service
              href="https://www.instagram.com/starthackclub"
              icon="instagram"
            />
            <Service
              href="https://www.facebook.com/Hack-Club-741805665870458"
              icon="facebook"
            />
            <Service href="mailto:team@hackclub.com" icon="mail_outline" />
          </Flex>
          <Text mt={2}>
            <Text is={Link} to="/team" color="info">Meet our team</Text>
          </Text>
        </RightCol>
      </Row>
      <Row w={1} wrap>
        <LeftCol>
          <Subhead>Hack Club</Subhead>
        </LeftCol>
        <RightCol>
          <Text>
            576 Natoma St
San Francisco, CA 94103
          </Text>
          <Text mt={2}>Nonprofit EIN: 81-2908499</Text>
          <Text mt={2}>
            <Text is="a" href="https://conduct.hackclub.com" color="info">
              Read our Code of Conduct
            </Text>
          </Text>
          <Text f={1} color="muted" mt={3}>
            © 2017 Hack Club
          </Text>
        </RightCol>
      </Row>
    </Container>
  </Base>
)

export default Footer
<commit_msg>Remove Meet our team link<commit_after>import React from 'react'
import { Subhead, Container, Flex, Box, Text, Row, Column } from 'rebass'
import { Link } from 'react-static'
import Icon from './Icon'
import { mx, geo, grays } from '../theme'

const Base = Box.extend.attrs({ is: 'footer', py: 5, px: 2 })`
  ${geo(grays.snow)};
`

const LeftCol = Column.extend.attrs({ w: [1, 0.4], mb: 2 })`
  ${mx[1]} {
    text-align: right;
  }
`
const RightCol = Column.extend.attrs({ w: [1, 0.6] })`
  text-align: left;
`

const Service = ({ href, icon, fill = 'muted', size = 32, ...props }) => (
  <Box is="a" target="_blank" href={href} mx={2} {...props}>
    <Icon name={icon} size={size} fill={fill} />
  </Box>
)

const Footer = () => (
  <Base>
    <Container>
      <Row w={1} wrap>
        <LeftCol>
          <Subhead>Join the Club</Subhead>
        </LeftCol>
        <RightCol>
          <Flex align="center" mx={-2}>
            <Service href="/slack_invite" icon="slack" />
            <Service href="https://twitter.com/starthackclub" icon="twitter" />
            <Service href="https://github.com/hackclub" icon="github" />
            <Service
              href="https://www.instagram.com/starthackclub"
              icon="instagram"
            />
            <Service
              href="https://www.facebook.com/Hack-Club-741805665870458"
              icon="facebook"
            />
            <Service href="mailto:team@hackclub.com" icon="mail_outline" />
          </Flex>
        </RightCol>
      </Row>
      <Row w={1} wrap>
        <LeftCol>
          <Subhead>Hack Club</Subhead>
        </LeftCol>
        <RightCol>
          <Text>
            576 Natoma St
San Francisco, CA 94103
          </Text>
          <Text mt={2}>Nonprofit EIN: 81-2908499</Text>
          <Text mt={2}>
            <Text is="a" href="https://conduct.hackclub.com" color="info">
              Read our Code of Conduct
            </Text>
          </Text>
          <Text f={1} color="muted" mt={3}>
            © 2017 Hack Club
          </Text>
        </RightCol>
      </Row>
    </Container>
  </Base>
)

export default Footer
<|endoftext|>


<commit_before>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/**
 * Creates a gap bridge iframe used to notify the native code about queued
 * commands.
 *
 * @private
 */
var cordova = require('cordova'),
    channel = require('cordova/channel'),
    utils = require('cordova/utils'),
    jsToNativeModes = {
        IFRAME_NAV: 0,
        XHR_NO_PAYLOAD: 1,
        XHR_WITH_PAYLOAD: 2,
        XHR_OPTIONAL_PAYLOAD: 3
    },
    bridgeMode,
    execIframe,
    execXhr,
    requestCount = 0,
    vcHeaderValue = null,
    commandQueue = [], // Contains pending JS->Native messages.
    isInContextOfEvalJs = 0;

function createExecIframe() {
    var iframe = document.createElement("iframe");
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
    return iframe;
}

function shouldBundleCommandJson() {
    if (bridgeMode == jsToNativeModes.XHR_WITH_PAYLOAD) {
        return true;
    }
    if (bridgeMode == jsToNativeModes.XHR_OPTIONAL_PAYLOAD) {
        var payloadLength = 0;
        for (var i = 0; i < commandQueue.length; ++i) {
            payloadLength += commandQueue[i].length;
        }
        // The value here was determined using the benchmark within CordovaLibApp on an iPad 3.
        return payloadLength < 4500;
    }
    return false;
}

function iOSExec() {
    // XHR mode does not work on iOS 4.2, so default to IFRAME_NAV for such devices.
    // XHR mode's main advantage is working around a bug in -webkit-scroll, which
    // doesn't exist in 4.X devices anyways.
    if (bridgeMode === undefined) {
        bridgeMode = navigator.userAgent.indexOf(' 4_') == -1 ? jsToNativeModes.XHR_NO_PAYLOAD : jsToNativeModes.IFRAME_NAV;
    }

    var successCallback, failCallback, service, action, actionArgs, splitCommand;
    var callbackId = null;
    if (typeof arguments[0] !== "string") {
        // FORMAT ONE
        successCallback = arguments[0];
        failCallback = arguments[1];
        service = arguments[2];
        action = arguments[3];
        actionArgs = arguments[4];

        // Since we need to maintain backwards compatibility, we have to pass
        // an invalid callbackId even if no callback was provided since plugins
        // will be expecting it. The Cordova.exec() implementation allocates
        // an invalid callbackId and passes it even if no callbacks were given.
        callbackId = 'INVALID';
    } else {
        // FORMAT TWO
        splitCommand = arguments[0].split(".");
        action = splitCommand.pop();
        service = splitCommand.join(".");
        actionArgs = Array.prototype.splice.call(arguments, 1);
    }

    // Register the callbacks and add the callbackId to the positional
    // arguments if given.
    if (successCallback || failCallback) {
        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
            {success:successCallback, fail:failCallback};
    }

    // Binary!
    var encodeArrayBufferAsBase64 = function(ab) {
        var arr = new Uint8Array(ab);
        var binary = '';
        var len = arr.length;
        for (var i = 0; i < len; i++) {
            binary += String.fromCharCode(arr[i]);
        }
        return window.btoa(binary);
    };
    actionArgs.forEach(function(arg, i) {
        if (Object.prototype.toString.call(arg) != Object.prototype.toString.call(new ArrayBuffer())) {
            return;
        }
        actionArgs[i] = {
            'CDVType': 'ArrayBuffer',
            'data': encodeArrayBufferAsBase64(arg)
        };
    });

    var command = [callbackId, service, action, actionArgs];

    // Stringify and queue the command. We stringify to command now to
    // effectively clone the command arguments in case they are mutated before
    // the command is executed.
    commandQueue.push(JSON.stringify(command));

    // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
    // then the queue will be flushed when it returns; no need for a poke.
    // Also, if there is already a command in the queue, then we've already
    // poked the native side, so there is no reason to do so again.
    if (!isInContextOfEvalJs && commandQueue.length == 1) {
        if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
            // This prevents sending an XHR when there is already one being sent.
            // This should happen only in rare circumstances (refer to unit tests).
            if (execXhr && execXhr.readyState != 4) {
                execXhr = null;
            }
            // Re-using the XHR improves exec() performance by about 10%.
            execXhr = execXhr || new XMLHttpRequest();
            // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
            // For some reason it still doesn't work though...
            execXhr.open('HEAD', "/!gap_exec", true);
            if (!vcHeaderValue) {
                vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
            }
            execXhr.setRequestHeader('vc', vcHeaderValue);
            execXhr.setRequestHeader('rc', ++requestCount);
            if (shouldBundleCommandJson()) {
                execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
            }
            execXhr.send(null);
        } else {
            execIframe = execIframe || createExecIframe();
            execIframe.src = "gap://ready";
        }
    }
}

iOSExec.jsToNativeModes = jsToNativeModes;

iOSExec.setJsToNativeBridgeMode = function(mode) {
    // Remove the iFrame since it may be no longer required, and its existence
    // can trigger browser bugs.
    // https://issues.apache.org/jira/browse/CB-593
    if (execIframe) {
        execIframe.parentNode.removeChild(execIframe);
        execIframe = null;
    }
    bridgeMode = mode;
};

iOSExec.nativeFetchMessages = function() {
    // Each entry in commandQueue is a JSON string already.
    if (!commandQueue.length) {
        return '';
    }
    var json = '[' + commandQueue.join(',') + ']';
    commandQueue.length = 0;
    return json;
};

iOSExec.nativeCallback = function(callbackId, status, payload, keepCallback) {
    return iOSExec.nativeEvalAndFetch(function() {
        var success = status === 0 || status === 1;
        cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
    });
};

iOSExec.nativeEvalAndFetch = function(func) {
    // This shouldn't be nested, but better to be safe.
    isInContextOfEvalJs++;
    try {
        func();
        return iOSExec.nativeFetchMessages();
    } finally {
        isInContextOfEvalJs--;
    }
};

module.exports = iOSExec;
<commit_msg>[ios] CB-2189: Implement ArrayBuffer native->js.<commit_after>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/**
 * Creates a gap bridge iframe used to notify the native code about queued
 * commands.
 *
 * @private
 */
var cordova = require('cordova'),
    channel = require('cordova/channel'),
    utils = require('cordova/utils'),
    jsToNativeModes = {
        IFRAME_NAV: 0,
        XHR_NO_PAYLOAD: 1,
        XHR_WITH_PAYLOAD: 2,
        XHR_OPTIONAL_PAYLOAD: 3
    },
    bridgeMode,
    execIframe,
    execXhr,
    requestCount = 0,
    vcHeaderValue = null,
    commandQueue = [], // Contains pending JS->Native messages.
    isInContextOfEvalJs = 0;

function createExecIframe() {
    var iframe = document.createElement("iframe");
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
    return iframe;
}

function shouldBundleCommandJson() {
    if (bridgeMode == jsToNativeModes.XHR_WITH_PAYLOAD) {
        return true;
    }
    if (bridgeMode == jsToNativeModes.XHR_OPTIONAL_PAYLOAD) {
        var payloadLength = 0;
        for (var i = 0; i < commandQueue.length; ++i) {
            payloadLength += commandQueue[i].length;
        }
        // The value here was determined using the benchmark within CordovaLibApp on an iPad 3.
        return payloadLength < 4500;
    }
    return false;
}

function massageArgsJsToNative(args) {
    var encodeArrayBufferAs8bitString = function(ab) {
        return String.fromCharCode.apply(null, new Uint8Array(ab));
    };
    var encodeArrayBufferAsBase64 = function(ab) {
        return window.btoa(encodeArrayBufferAs8bitString(ab));
    };
    args.forEach(function(arg, i) {
        if (arg instanceof ArrayBuffer) {
            args[i] = {
                'CDVType': 'ArrayBuffer',
                'data': encodeArrayBufferAsBase64(arg)
            };
        } // else if (arg instanceof OTHER_TYPE)
    });
    return args;
}

function massagePayloadNativeToJs(payload) {
    if (payload && payload.hasOwnProperty('CDVType') && payload.CDVType == 'ArrayBuffer') {
        var stringToArrayBuffer = function(str) {
            var ret = new Uint8Array(str.length);
            for (var i = 0; i < str.length; i++) {
                ret[i] = str.charCodeAt(i);
            }
            return ret.buffer;
        };
        var base64ToArrayBuffer = function(b64) {
            return stringToArrayBuffer(atob(b64));
        };
        payload = base64ToArrayBuffer(payload.data);
    }
    return payload;
}

function iOSExec() {
    // XHR mode does not work on iOS 4.2, so default to IFRAME_NAV for such devices.
    // XHR mode's main advantage is working around a bug in -webkit-scroll, which
    // doesn't exist in 4.X devices anyways.
    if (bridgeMode === undefined) {
        bridgeMode = navigator.userAgent.indexOf(' 4_') == -1 ? jsToNativeModes.XHR_NO_PAYLOAD : jsToNativeModes.IFRAME_NAV;
    }

    var successCallback, failCallback, service, action, actionArgs, splitCommand;
    var callbackId = null;
    if (typeof arguments[0] !== "string") {
        // FORMAT ONE
        successCallback = arguments[0];
        failCallback = arguments[1];
        service = arguments[2];
        action = arguments[3];
        actionArgs = arguments[4];

        // Since we need to maintain backwards compatibility, we have to pass
        // an invalid callbackId even if no callback was provided since plugins
        // will be expecting it. The Cordova.exec() implementation allocates
        // an invalid callbackId and passes it even if no callbacks were given.
        callbackId = 'INVALID';
    } else {
        // FORMAT TWO
        splitCommand = arguments[0].split(".");
        action = splitCommand.pop();
        service = splitCommand.join(".");
        actionArgs = Array.prototype.splice.call(arguments, 1);
    }

    // Register the callbacks and add the callbackId to the positional
    // arguments if given.
    if (successCallback || failCallback) {
        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
            {success:successCallback, fail:failCallback};
    }

    actionArgs = massageArgsJsToNative(actionArgs);

    var command = [callbackId, service, action, actionArgs];

    // Stringify and queue the command. We stringify to command now to
    // effectively clone the command arguments in case they are mutated before
    // the command is executed.
    commandQueue.push(JSON.stringify(command));

    // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
    // then the queue will be flushed when it returns; no need for a poke.
    // Also, if there is already a command in the queue, then we've already
    // poked the native side, so there is no reason to do so again.
    if (!isInContextOfEvalJs && commandQueue.length == 1) {
        if (bridgeMode != jsToNativeModes.IFRAME_NAV) {
            // This prevents sending an XHR when there is already one being sent.
            // This should happen only in rare circumstances (refer to unit tests).
            if (execXhr && execXhr.readyState != 4) {
                execXhr = null;
            }
            // Re-using the XHR improves exec() performance by about 10%.
            execXhr = execXhr || new XMLHttpRequest();
            // Changing this to a GET will make the XHR reach the URIProtocol on 4.2.
            // For some reason it still doesn't work though...
            execXhr.open('HEAD', "/!gap_exec", true);
            if (!vcHeaderValue) {
                vcHeaderValue = /.*\((.*)\)/.exec(navigator.userAgent)[1];
            }
            execXhr.setRequestHeader('vc', vcHeaderValue);
            execXhr.setRequestHeader('rc', ++requestCount);
            if (shouldBundleCommandJson()) {
                execXhr.setRequestHeader('cmds', iOSExec.nativeFetchMessages());
            }
            execXhr.send(null);
        } else {
            execIframe = execIframe || createExecIframe();
            execIframe.src = "gap://ready";
        }
    }
}

iOSExec.jsToNativeModes = jsToNativeModes;

iOSExec.setJsToNativeBridgeMode = function(mode) {
    // Remove the iFrame since it may be no longer required, and its existence
    // can trigger browser bugs.
    // https://issues.apache.org/jira/browse/CB-593
    if (execIframe) {
        execIframe.parentNode.removeChild(execIframe);
        execIframe = null;
    }
    bridgeMode = mode;
};

iOSExec.nativeFetchMessages = function() {
    // Each entry in commandQueue is a JSON string already.
    if (!commandQueue.length) {
        return '';
    }
    var json = '[' + commandQueue.join(',') + ']';
    commandQueue.length = 0;
    return json;
};

iOSExec.nativeCallback = function(callbackId, status, payload, keepCallback) {
    return iOSExec.nativeEvalAndFetch(function() {
        var success = status === 0 || status === 1;
        payload = massagePayloadNativeToJs(payload);
        cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
    });
};

iOSExec.nativeEvalAndFetch = function(func) {
    // This shouldn't be nested, but better to be safe.
    isInContextOfEvalJs++;
    try {
        func();
        return iOSExec.nativeFetchMessages();
    } finally {
        isInContextOfEvalJs--;
    }
};

module.exports = iOSExec;
<|endoftext|>


<commit_before>/**
 * Usage : m(Router, {route: RouteSpec, sinkNames: [...]}, [children
 * components])
 */

import { assertContract, assertSignature, isArray, isArrayOf, isFunction, isString } from "../utils"
import { m } from "./m"
import { isNil, map as mapR, mergeAll as mergeAllR, omit, path as pathR } from "ramda"
import { routeMatcher } from "../vendor/routematcher"
import Rx from "rx"
const $ = Rx.Observable

// Configuration
const routeSourceName = 'route$'

///////////
// Helpers
function match(routeToMatch) {
  let rm1 = routeMatcher(routeToMatch)
  let rm2 = routeMatcher(routeToMatch + '/*routeRemainder')

  return function match(incomingRoute) {
    if (isNil(incomingRoute)) {
      return {
        match: null
      }
    }

    const matched = rm1.parse(incomingRoute)
    const remainder = rm2.parse(incomingRoute)

    return {
      match: matched || remainder
    }
  }
}

function isRouteSettings(obj) {
  return obj.route && isString(obj.route) &&
    obj.sinkNames && isArray(obj.sinkNames) && obj.sinkNames.length > 0
}

/**
 * Definition for a router component which :
 * - will pass the sinks of its children components iff the new route
 * matches the route configured for the components
 * - when the route no longer matches, components sinks are terminated
 * - when the route matches, changes but keeps the same value, children
 * sinks remain in place
 * Route information is read on the `route$` property
 * Children components pass to their own children a `route$` which is the
 * `route$` they received from their parent, from which they remove the
 * part of the route that they have matched (passing what is called here the
 * remainder).
 * Params parsed from the matched route are passed to the children
 * component through their `settings` parameters, with the `routeParams`
 * property.
 * The `route$` property can be but should not be manipulated directly out
 * of a `Router` component.
 *
 * Two settings are necessary :
 * - route : the route which triggers the component sinks activation.
 *   1. Note that a route value of `undefined` will produce no matching,
 *   while a value of `""` will match `":user"` ! See the tests
 *   2. Every new nested route will trigger the emission of a nested route
 *   value, even if that new nested route value is the same as the
 *   previous one.
 *   3. In the routed component, the `route$` will emit the matched
 *   portion of the route. However, the same information is already broken
 *   down in `routeParams` and should be read from there.
 *
 * - sinkNames : the list of sinks (names) which must be activated in
 * response to the matching route
 *
 * Note that the DOM sink will emit null on some specific circumstances,
 * hence the component receiving the routed DOM sink must plan for that
 * case accordingly. That means DOM :: Observable<VNode>|Null
 *
 * @param {Sources} sources
 * @param {{route: string, sinkNames: Array<string>, trace: string}} settings
 * @param {Array<Component>} childrenComponents
 * @param {function(Sources, Settings)} makeOwnSinks
 *
 */
export function computeSinks(makeOwnSinks, childrenComponents, sources, settings) {
  console.groupCollapsed('Router component > makeAllSinks')
  console.log('sources, settings, childrenComponents', sources, settings, childrenComponents);

  const signature = [{ settings: isRouteSettings },]

  assertSignature('Router > computeSinks', [settings], signature)

  // The sink names are necessary as we cannot know otherwise in
  // advance what are the sinks output by the children components without
  // executing all the children components.
  // However to execute the children components, we need to pass the route
  // params to the children. To get those params, in turn, we need to
  // enter the observable monad, from which we can't get out.
  // This behaviour results in having to handle null cases for sinks (some
  // sinks might be present only on some children components).
  const sinkNames = settings.sinkNames
  const trace = 'router:' + (settings.trace || "")

  let route$ = sources[routeSourceName]
    .tap(console.error.bind(console, 'route$'))

  let matchedRoute$ = route$.map(match(settings.route))
    .tap(console.warn.bind(console, trace + '|matchedRoute$'))
    // NOTE : replaying here is mandatory
    // That's because the children are passed `matchedRoute` and
    // connect to it AFTER the `route$` has emitted its value...
    // In short, while time is abstracted out in the case of a static
    // graph, dynamic stream graphs come with synchronization pains
    .shareReplay(1)

  let changedRouteEvents$ = matchedRoute$
    .pluck('match')
    .distinctUntilChanged(x => {
      console.log('distinctUntilChanged on : ', x ? omit(['routeRemainder'], x) : null)
      return x ? omit(['routeRemainder'], x) : null
    })
    .tap(console.warn.bind(console, 'changedRouteEvents$'))
    .share()
  // Note : must be shared, used twice here

  const cachedSinks$ = changedRouteEvents$
    .map(function (params) {
      let cachedSinks

      if (params != null) {
        console.info('computing children components sinks', params)
        const componentFromChildren = m({
            makeLocalSources: function makeLocalSources(sources, __settings) {
              console.group('makeLocalSources')
              console.log('sources, __settings', sources, __settings);
              console.groupEnd('makeLocalSources')

              return {
                route$: matchedRoute$
                  .map(pathR(['match', 'routeRemainder']))
                  .tap(console.warn.bind(console, settings.trace + ' :' +
                    ' changedRouteEvents$' +
                    ' : routeRemainder: '))
                  .share(),
              }
            },
          }, {
            routeParams: omit(['routeRemainder'], params),
            trace: 'inner - ' + trace
          },
          childrenComponents)
        cachedSinks = componentFromChildren(sources, settings)
      }
      else {
        cachedSinks = null
      }

      return cachedSinks
    })
    .share()

  function makeRoutedSinkFromCache(sinkName) {
    return function makeRoutedSinkFromCache(params, cachedSinks) {
      let cached$, preCached$, prefix$

      if (params != null) {
        // Case : new route matches component configured route
        if (cachedSinks[sinkName] != null) {
          // Case : the component produces a sink with that name
          // This is an important case, as parent can have children
          // nested at arbitrary levels, with either :
          // 1. sinks which will not be retained (not in `sinkNames`
          // settings)
          // 2. or no sinks matching a particular `sinkNames`
          // Casuistic 1. is taken care of automatically as we only
          // construct the sinks in `sinkNames`
          // Casuistic 2. is taken care of thereafter

          prefix$ = sinkName === 'DOM' ?
            // Case : DOM sink
            // actually any sink which is merged with a `combineLatest`
            // but here by default only DOM sinks are merged that way
            // Because the `combineLatest` blocks till all its sources
            // have started, and that behaviour interacts badly with
            // route changes desired behavior, we forcibly emits a `null`
            // value at the beginning of every sink.
            $.of(null) :
            // Case : Non-DOM sink
            // Non-DOM sinks are merged with a simple `merge`, there
            // is no conflict here, so we just return nothing
            $.empty()

          preCached$ = cachedSinks[sinkName]
            .tap(console.log.bind(console, 'sink ' + sinkName + ':'))
            .finally(_ => {
              void _
              console.log(trace + ' : sink ' + sinkName + ': terminating due to' +
                ' route change')
            })

          cached$ = $.concat(prefix$, preCached$)
        }
        else {
          // Case : the component does not have any sinks with the
          // corresponding sinkName
          cached$ = $.empty()
        }
      }
      else {
        // Case : new route does NOT match component configured route
        console.log('params is null!!! no match for this component on' +
          ' this route :' + trace)
        cached$ = sinkName === 'DOM' ? $.of(null) : $.empty()
      }

      return cached$
    }
  }

  function makeRoutedSink(sinkName) {
    return {
      [sinkName]: changedRouteEvents$.withLatestFrom(
        cachedSinks$,
        makeRoutedSinkFromCache(sinkName)
      ).switch()
    }
  }

  console.groupEnd('makeAllSinks')
  return mergeAllR(mapR(makeRoutedSink, sinkNames))
}

export function checkRouteSettingsHaveRouteProp(settings) {
  // there must be a route property and it must be a string
  return settings.route && isString(settings.route)
}

// TODO : think about some rules for names for this kind of functions (HOC? not totally)
// 1. I need an array of component for nested routing
// onRoute(URL, [onRoute(url1, chilcComp1), onRoute(url2, childComp2)])
// 2. But then I miss the settings parameter, i.e. I need to merge the children sinks with an
// appropriate default...
// That is combineLatest for the behaviours (DOM...), merge for the events
// TODO : check the current defaults of `m`
export function onRoute(settings, components) {
  // check that components is an array
  assertContract(isArrayOf(isFunction), [components], `onRoute : MUST be passed array of functions (components)`);
  // check that settings.route is set to a string
  assertContract(checkRouteSettingsHaveRouteProp, [settings], `onRoute : settings MUST include the url in its route property!`)

  return m({ computeSinks }, settings, components)
}

// TODO : have an index.js which imports stuff from sublibs and export * them out
// TODO : then make a npm release, put that in a package.json in FSM-example
<commit_msg>synchronized router.js vs last version<commit_after>/**
 * Usage : m(Router, {route: RouteSpec, sinkNames: [...]}, [children
 * components])
 */

import { assertContract, assertSignature, isArray, isArrayOf, isFunction, isString } from "../utils"
import { m } from "./m"
import { isNil, map as mapR, mergeAll as mergeAllR, omit, path as pathR, keys } from "ramda"
import { routeMatcher } from "../vendor/routematcher"
import Rx from "rx"
const $ = Rx.Observable

// Configuration
const routeSourceName = 'route$'

///////////
// Helpers
function match(routeToMatch) {
  let rm1 = routeMatcher(routeToMatch)
  let rm2 = routeMatcher(routeToMatch + '/*routeRemainder')

  return function match(incomingRoute) {
    if (isNil(incomingRoute)) {
      return {
        match: null
      }
    }

    const matched = rm1.parse(incomingRoute)
    const remainder = rm2.parse(incomingRoute)

    return {
      match: matched || remainder
    }
  }
}

function isRouteSettings(obj) {
  return obj.route && isString(obj.route) &&
    obj.sinkNames && isArray(obj.sinkNames) && obj.sinkNames.length > 0
}

/**
 * Definition for a router component which :
 * - will pass the sinks of its children components iff the new route
 * matches the route configured for the components
 * - when the route no longer matches, components sinks are terminated
 * - when the route matches, changes but keeps the same value, children
 * sinks remain in place
 * Route information is read on the `route$` property
 * Children components pass to their own children a `route$` which is the
 * `route$` they received from their parent, from which they remove the
 * part of the route that they have matched (passing what is called here the
 * remainder).
 * Params parsed from the matched route are passed to the children
 * component through their `settings` parameters, with the `routeParams`
 * property.
 * The `route$` property can be but should not be manipulated directly out
 * of a `Router` component.
 *
 * Two settings are necessary :
 * - route : the route which triggers the component sinks activation.
 *   1. Note that a route value of `undefined` will produce no matching,
 *   while a value of `""` will match `":user"` ! See the tests
 *   2. Every new nested route will trigger the emission of a nested route
 *   value, even if that new nested route value is the same as the
 *   previous one.
 *   3. In the routed component, the `route$` will emit the matched
 *   portion of the route. However, the same information is already broken
 *   down in `routeParams` and should be read from there.
 *
 * - sinkNames : the list of sinks (names) which must be activated in
 * response to the matching route
 *
 * Note that the DOM sink will emit null on some specific circumstances,
 * hence the component receiving the routed DOM sink must plan for that
 * case accordingly. That means DOM :: Observable<VNode>|Null
 *
 * @param {Sources} sources
 * @param {{route: string, sinkNames: Array<string>, trace: string}} settings
 * @param {Array<Component>} childrenComponents
 * @param {function(Sources, Settings)} makeOwnSinks
 *
 */
export function computeSinks(makeOwnSinks, childrenComponents, sources, settings) {
  console.groupCollapsed('Router > computeSinks')
  console.log(`sources : ${keys(sources)}`)
  console.log(`settings`, settings)
  console.log('childrenComponents', childrenComponents);

  const signature = [{ settings: isRouteSettings },]

  assertSignature('Router > computeSinks', [settings], signature)

  // The sink names are necessary as we cannot know otherwise in
  // advance what are the sinks output by the children components without
  // executing all the children components.
  // However to execute the children components, we need to pass the route
  // params to the children. To get those params, in turn, we need to
  // enter the observable monad, from which we can't get out.
  // This behaviour results in having to handle null cases for sinks (some
  // sinks might be present only on some children components).
  const sinkNames = settings.sinkNames
  const trace = 'Router (' + (settings.trace || "") + ")"

  let route$ = sources[routeSourceName]
    .tap(console.debug.bind(console, `${trace} : route$`))

  let matchedRoute$ = route$.map(match(settings.route))
    .tap(console.debug.bind(console, `${trace} : matchedRoute$`))
    // NOTE : replaying here is mandatory
    // That's because the children are passed `matchedRoute` and
    // connect to it AFTER the `route$` has emitted its value...
    // In short, while time is abstracted out in the case of a static
    // graph, dynamic stream graphs come with synchronization pains
    .shareReplay(1)

  let changedRouteEvents$ = matchedRoute$
    .pluck('match')
    .distinctUntilChanged(x => {
      console.debug('distinctUntilChanged on : ', x ? omit(['routeRemainder'], x) : null)
      return x ? omit(['routeRemainder'], x) : null
    })
    .tap(console.debug.bind(console, `${trace} : changedRouteEvents$`))
    .share()
  // Note : must be shared, used twice here

  const cachedSinks$ = changedRouteEvents$
    .map(function (params) {
      let cachedSinks

      if (params != null) {
        console.info('computing children components sinks', params)
        const componentFromChildren = m({
            makeLocalSources: function makeLocalSources(sources, __settings) {
              console.group('${trace} : changedRouteEvents$ : children wrapper component :' +
                ' makeLocalSources')
              console.log(`sources : ${keys(sources)}, __settings :`, __settings);
              console.groupEnd()

              return {
                route$: matchedRoute$
                  .map(pathR(['match', 'routeRemainder']))
                  .tap(console.debug.bind(console,
                    `${trace} : changedRouteEvents$ : children wrapper component  : routeRemainder (new route$ for children)`))
                  .share(),
              }
            },
          }, {
            routeParams: omit(['routeRemainder'], params),
            trace: 'inner - ' + trace
          },
          childrenComponents)
        cachedSinks = componentFromChildren(sources, settings)
      }
      else {
        cachedSinks = null
      }

      return cachedSinks
    })
    .share()

  function makeRoutedSinkFromCache(sinkName) {
    return function makeRoutedSinkFromCache(params, cachedSinks) {
      let cached$, preCached$, prefix$

      if (params != null) {
        // Case : new route matches component configured route
        if (cachedSinks[sinkName] != null) {
          // Case : the component produces a sink with that name
          // This is an important case, as parent can have children
          // nested at arbitrary levels, with either :
          // 1. sinks which will not be retained (not in `sinkNames`
          // settings)
          // 2. or no sinks matching a particular `sinkNames`
          // Casuistic 1. is taken care of automatically as we only
          // construct the sinks in `sinkNames`
          // Casuistic 2. is taken care of thereafter

          prefix$ = sinkName === 'DOM' ?
            // Case : DOM sink
            // actually any sink which is merged with a `combineLatest`
            // but here by default only DOM sinks are merged that way
            // Because the `combineLatest` blocks till all its sources
            // have started, and that behaviour interacts badly with
            // route changes desired behavior, we forcibly emits a `null`
            // value at the beginning of every sink.
            $.of(null) :
            // Case : Non-DOM sink
            // Non-DOM sinks are merged with a simple `merge`, there
            // is no conflict here, so we just return nothing
            $.empty()

          preCached$ = cachedSinks[sinkName]
            .tap(console.log.bind(console, 'sink ' + sinkName + ':'))
            .finally(_ => {
              void _
              console.log(`${trace} : sink ${sinkName} : terminating due to route change'`)
            })

          cached$ = $.concat(prefix$, preCached$)
        }
        else {
          // Case : the component does not have any sinks with the
          // corresponding sinkName
          cached$ = $.empty()
        }
      }
      else {
        // Case : new route does NOT match component configured route
        console.log(`${trace} : params is null!!! no match for this component on this route`)
        cached$ = sinkName === 'DOM' ? $.of(null) : $.empty()
      }

      return cached$
    }
  }

  function makeRoutedSink(sinkName) {
    return {
      [sinkName]: changedRouteEvents$.withLatestFrom(
        cachedSinks$,
        makeRoutedSinkFromCache(sinkName)
      ).switch()
    }
  }

  console.groupEnd('makeAllSinks')
  return mergeAllR(mapR(makeRoutedSink, sinkNames))
}

export function checkRouteSettingsHaveRouteProp(settings) {
  // there must be a route property and it must be a string
  return settings.route && isString(settings.route)
}

// TODO : think about some rules for names for this kind of functions (HOC? not totally)
// 1. I need an array of component for nested routing
// onRoute(URL, [onRoute(url1, chilcComp1), onRoute(url2, childComp2)])
// 2. But then I miss the settings parameter, i.e. I need to merge the children sinks with an
// appropriate default...
// That is combineLatest for the behaviours (DOM...), merge for the events
// TODO : check the current defaults of `m`
export function onRoute(settings, components) {
  // check that components is an array
  assertContract(isArrayOf(isFunction), [components], `onRoute : MUST be passed array of functions (components)`);
  // check that settings.route is set to a string
  assertContract(checkRouteSettingsHaveRouteProp, [settings], `onRoute : settings MUST include the url in its route property!`)

  return m({ computeSinks }, settings, components)
}

// TODO : have an index.js which imports stuff from sublibs and export * them out
// TODO : then make a npm release, put that in a package.json in FSM-example
<|endoftext|>