<commit_before>// Load modules

var Hoek = require('hoek');
var Boom = require('boom');

// Declare internals

var internals = {
    defaults: {
        isCaseSensitive: true,
        cors: null
    }
};

exports.Router = internals.Router = function (options) {

    this.settings = Hoek.applyToDefaults(internals.defaults, options || {});

    this.routes = {};                                // Key: HTTP method or * for catch-all, value: sorted array of routes
    this.vhosts = null;                              // {} where Key: hostname, value: see this.routes
};

internals.Router.prototype.add = function (config, route) {

    var self = this;

    var method = config.method.toLowerCase();

    var vhost = config.vhost || '*';
    if (vhost !== '*') {
        self.vhosts = self.vhosts || {};
        self.vhosts[vhost] = self.vhosts[vhost] || {};
    }

    var table = (vhost === '*' ? self.routes : self.vhosts[vhost]);
    table[method] = table[method] || [];

    var analysis = config.analysis || this.analyze(config.path);
    var record = {
        path: config.path,
        route: route,
        segments: analysis.segments,
        params: analysis.params,
        fingerprint: analysis.fingerprint
    };

    // Check for existing route with same fingerprint

    var altFingerprint = (record.segments[record.segments.length - 1].isEmptyOk ? record.fingerprint.substring(0, record.fingerprint.length - 2) : '');
    table[method].forEach(function (existing) {

        Hoek.assert(record.fingerprint !== existing.fingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);
        Hoek.assert(altFingerprint !== existing.fingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);

        var altExistingFingerprint = (existing.segments[existing.segments.length - 1].isEmptyOk ? existing.fingerprint.substring(0, existing.fingerprint.length - 2) : '');
        Hoek.assert(record.fingerprint !== altExistingFingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);
    });

    // Add and sort

    table[method].push(record);
    table[method].sort(internals.sort);
    return record;
};

internals.Router.prototype.route = function (method, path, hostname) {

    var vhost = (this.vhosts && hostname && this.vhosts[hostname]);
    var route = (vhost && this._lookup(path, vhost, method)) ||
                this._lookup(path, this.routes, method) ||
                (method === 'head' && vhost && this._lookup(path, vhost, 'get')) ||
                (method === 'head' && this._lookup(path, this.routes, 'get')) ||
                (method === 'options' && this.settings.cors) ||
                (vhost && this._lookup(path, vhost, '*')) ||
                this._lookup(path, this.routes, '*') ||
                Boom.notFound();

    return route;
};

internals.Router.prototype._lookup = function (path, table, method) {

    var match = false;
    var routes = table[method];
    if (routes) {
        var pathSegments = path.split('/');
        for (var i = 0, il = routes.length; !match && i < il; ++i) {
            var record = routes[i];
            match = this._match(record, path, pathSegments);                    // Returns Error, false, or result object
            if (match &&
                !match.isBoom) {

                match.route = record.route;
            }
        }
    }

    return match;
};

internals.Router.prototype._match = function (record, path, pathSegments) {

    var result = {
        params: {},
        paramsArray: []
    };

    // Literal comparison

    if (!record.params.length) {
        return (record.path === (this.settings.isCaseSensitive ? path : path.toLowerCase()) ? result : false);
    }

    // Mismatching segment count

    var pl = pathSegments.length - 1;
    var sl = record.segments.length;
    var last = record.segments[sl - 1];

    if (pl !== sl &&                                                        // Different count
        (pl !== sl - 1 || (!last.isEmptyOk && !last.isWildcard)) &&         // Not short one with empty or wildcard allowed
        (pl < sl || !last.isWildcard)) {

        return false;
    }

    // Parameter matching

    var match = true;
    for (var i = 0; match && (match instanceof Error === false) && i < record.segments.length; ++i) {
        var segment = record.segments[i];
        if (segment.isWildcard) {
            match = internals.setParam(segment.name, pathSegments.slice(i + 1).join('/'), result, true);
        }
        else if (segment.count) {
            match = internals.setParam(segment.name, pathSegments.slice(i + 1, i + 1 + segment.count).join('/'), result, false);
            i += (segment.count - 1);
        }
        else if (segment.name) {
            if (segment.extract) {
                var partial = pathSegments[i + 1].match(segment.extract);
                if (!partial) {
                    match = false;
                }
                else {
                    match = internals.setParam(segment.name, partial[1], result, segment.isEmptyOk);
                }
            }
            else {
                match = internals.setParam(segment.name, pathSegments[i + 1], result, segment.isEmptyOk);
            }
        }
        else {
            match = (segment.literal === (this.settings.isCaseSensitive ? pathSegments[i + 1] : pathSegments[i + 1].toLowerCase()));
        }
    }

    if (match !== true) {           // Can be Error
        return match;
    }

    return result;
};

internals.setParam = function (name, value, result, isEmptyOk) {

    if (!isEmptyOk && !value) {
        return false;
    }

    if (isEmptyOk && !value) {
        return true;
    }

    try {
        var decoded = decodeURIComponent(value);
        result.params[name] = decoded;
        result.paramsArray.push(decoded);
        return true;
    }
    catch (err) {
        return Boom.badRequest('Invalid request path');
    }
};

internals.generatePathRegex = function () {

    /*
        /path/{param}/path/{param?}
        /path/{param*2}/path
        /path/{param*2}
        /path/x{param}x
        /{param*}
    */

    var empty = '(^\\/$)';

    var legalChars = '[\\w\\!\\$&\'\\(\\)\\*\\+\\,;\\=\\:@\\-\\.~]';
    var encoded = '%[A-F0-9]{2}';

    var literalChar = '(?:' + legalChars + '|' + encoded + ')';
    var literal = literalChar + '+';
    var literalOptional = literalChar + '*';

    var midParam = '(\\{\\w+(\\*[1-9]\\d*)?\\})';                                   // {p}, {p*2}
    var endParam = '(\\/(\\{\\w+((\\*([1-9]\\d*)?)|(\\?))?\\})?)?';                 // {p}, {p*2}, {p*}, {p?}
    var mixParam = '(\\{\\w+\\??\\})';                                              // {p}, {p?}

    var literalParam = '(' + literal + mixParam + literalOptional + ')|(' + literalOptional + mixParam + literal + ')';

    var segmentContent = '(' + literal + '|' + midParam + '|' + literalParam + ')';
    var segment = '\\/' + segmentContent;
    var segments = '(' + segment + ')*';

    var path = '(^' + segments + endParam + '$)';

    var parseParam = '^(' + literalOptional + ')' + '\\{(\\w+)(?:(\\*)(\\d+)?)?(\\?)?\\}' + '(' + literalOptional + ')$';

    var expressions = {
        parseParam: new RegExp(parseParam),                                        // $1: literal-pre, $2: name, $3: *, $4: segments, $5: empty-ok, $6: literal-post
        validatePath: new RegExp(empty + '|' + path),
        validatePathEncoded: /%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g
    };

    return expressions;
};

internals.pathRegex = internals.generatePathRegex();

internals.Router.prototype.analyze = function (path) {

    Hoek.assert(internals.pathRegex.validatePath.test(path), 'Invalid path:', path);
    Hoek.assert(!internals.pathRegex.validatePathEncoded.test(path), 'Path cannot contain encoded non-reserved path characters:', path);

    var pathParts = path.split('/');
    var segments = [];
    var params = {};
    var fingers = [];

    for (var i = 1, il = pathParts.length; i < il; ++i) {                            // Skip first empty segment
        var segment = pathParts[i];
        var param = segment.match(internals.pathRegex.parseParam);
        if (param) {

            // Parameter

            var pre = param[1];
            var name = param[2];
            var isMulti = !!param[3];
            var multiCount = param[4] && parseInt(param[4], 10);
            var isEmptyOk = !!param[5];
            var post = param[6];

            Hoek.assert(!params[name], 'Cannot repeat the same parameter name:', name, 'in:', path);
            params[name] = true;

            if (isMulti) {
                if (multiCount) {
                    for (var m = 0; m < multiCount; ++m) {
                        fingers.push('?');
                        segments.push({ name: name, count: multiCount });
                    }
                }
                else {
                    fingers.push('#');
                    segments.push({ isWildcard: true, name: name });
                }
            }
            else {
                fingers.push(pre + '?' + post);
                var segmentMeta = {
                    name: name,
                    isEmptyOk: isEmptyOk
                };

                if (pre || post) {
                    segmentMeta.mixed = true;
                    segmentMeta.pre = pre;
                    segmentMeta.post = post;
                    segmentMeta.extract = new RegExp('^' + Hoek.escapeRegex(pre) + '(.' + (isEmptyOk ? '*' : '+') + ')' + Hoek.escapeRegex(post) + '$', (!this.settings.isCaseSensitive ? 'i' : ''));
                }

                segments.push(segmentMeta);
            }
        }
        else {

            // Literal

            segment = this.settings.isCaseSensitive ? segment : segment.toLowerCase();
            fingers.push(segment);
            segments.push({ literal: segment });
        }
    }

    return {
        segments: segments,
        fingerprint: '/' + fingers.join('/'),
        params: Object.keys(params)
    }
};

internals.Router.prototype.table = function (host) {

    var result = [];
    var collect = function (table) {

        if (!table) {
            return;
        }

        Object.keys(table).forEach(function (method) {

            table[method].forEach(function (record) {

                result.push(record.route);
            });
        });
    };

    if (this.vhosts) {
        var vhosts = host ? [].concat(host) : Object.keys(this.vhosts);
        for (var i = 0, il = vhosts.length; i < il; ++i) {
            collect(this.vhosts[vhosts[i]]);
        }
    }

    collect(this.routes);

    return result;
};

internals.sort = function (a, b) {

    // Biased for less and shorter segments which are faster to compare

    var aFirst = -1;
    var bFirst = 1;

    // Prepare fingerprints

    var aFingers = a.segments;
    var bFingers = b.segments;

    var al = aFingers.length;
    var bl = bFingers.length;

    // Compare fingerprints

    if ((aFingers[al - 1].isWildcard) ^ (bFingers[bl - 1].isWildcard)) {
        return (aFingers[al - 1].isWildcard ? bFirst : aFirst);
    }

    var size = Math.min(al, bl);
    for (var i = 0; i < size; ++i) {

        var aSegment = aFingers[i];
        var bSegment = bFingers[i];

        // Equal (except both wildcards as that means entire route paths are equal)

        if ((aSegment.name && bSegment.name && !aSegment.isWildcard && !bSegment.isWildcard && !aSegment.mixed && !bSegment.mixed) ||
            (aSegment.literal !== undefined && aSegment.literal === bSegment.literal)) {

            continue;
        }

        // One is wildcard

        if (aSegment.isWildcard) {
            return bFirst;
        }
        else if (bSegment.isWildcard) {
            return aFirst;
        }

        // One is parameter -or- both and at least one is mixed

        if (aSegment.name) {
            if (bSegment.name &&
                al === bl) {

                var mixed = internals.compareMixed(aSegment, bSegment);
                if (mixed === 0) {
                    continue;
                }

                return mixed;
            }
            else {
                return (al >= bl ? bFirst : aFirst);        // Bias to literal over parameter
            }
        }
        else if (bSegment.name) {
            break;
        }

        // Both literal but different

        if (al === bl) {
            if (aSegment.literal.length === bSegment.literal.length) {
                return (aSegment.literal > bSegment.literal ? bFirst : aFirst);
            }

            return (aSegment.literal.length > bSegment.literal.length ? bFirst : aFirst);
        }

        // Less segments win

        break;
    }

    return (al > bl ? bFirst : aFirst);
};

internals.compareMixed = function (aSegment, bSegment, p) {

    p = p || 0;
    if (p === 3) {
        return 0;
    }

    var aFirst = -1;
    var bFirst = 1;

    var prop = ['mixed', 'pre', 'post'][p];
    var ap = aSegment[prop];
    var bp = bSegment[prop];

    if (ap === bp) {
        return internals.compareMixed(aSegment, bSegment, p + 1);
    }
    else {
        if (ap) {
            if (bp) {
                if (ap.length === bp.length) {
                    return (ap > bp ? bFirst : aFirst);
                }
                else {
                    return (ap.length > bp.length ? aFirst : bFirst);
                }
            }
            else {
                return aFirst;
            }
        }
        else {
            return bFirst;
        }
    }
};<commit_msg>special handlers<commit_after>// Load modules

var Hoek = require('hoek');
var Boom = require('boom');

// Declare internals

var internals = {
    defaults: {
        isCaseSensitive: true
    }
};

exports.Router = internals.Router = function (options) {

    this.settings = Hoek.applyToDefaults(internals.defaults, options || {});

    this.routes = {};                                // Key: HTTP method or * for catch-all, value: sorted array of routes
    this.vhosts = null;                              // {} where Key: hostname, value: see this.routes

    this.specials = {
        badRequest: null,
        notFound: null,
        options: null
    };
};

internals.Router.prototype.add = function (config, route) {

    var self = this;

    var method = config.method.toLowerCase();

    var vhost = config.vhost || '*';
    if (vhost !== '*') {
        self.vhosts = self.vhosts || {};
        self.vhosts[vhost] = self.vhosts[vhost] || {};
    }

    var table = (vhost === '*' ? self.routes : self.vhosts[vhost]);
    table[method] = table[method] || [];

    var analysis = config.analysis || this.analyze(config.path);
    var record = {
        path: config.path,
        route: route,
        segments: analysis.segments,
        params: analysis.params,
        fingerprint: analysis.fingerprint
    };

    // Check for existing route with same fingerprint

    var altFingerprint = (record.segments[record.segments.length - 1].isEmptyOk ? record.fingerprint.substring(0, record.fingerprint.length - 2) : '');
    table[method].forEach(function (existing) {

        Hoek.assert(record.fingerprint !== existing.fingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);
        Hoek.assert(altFingerprint !== existing.fingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);

        var altExistingFingerprint = (existing.segments[existing.segments.length - 1].isEmptyOk ? existing.fingerprint.substring(0, existing.fingerprint.length - 2) : '');
        Hoek.assert(record.fingerprint !== altExistingFingerprint, 'New route: ' + config.path + ' conflicts with existing: ' + existing.path);
    });

    // Add and sort

    table[method].push(record);
    table[method].sort(internals.sort);
    return record;
};

internals.Router.prototype.special = function (type, route) {

    Hoek.assert(Object.keys(this.specials).indexOf(type) !== -1, 'Unknown special route type:', type);

    this.specials[type] = { route: route };
};

internals.Router.prototype.route = function (method, path, hostname) {

    var vhost = (this.vhosts && hostname && this.vhosts[hostname]);
    var route = (vhost && this._lookup(path, vhost, method)) ||
                this._lookup(path, this.routes, method) ||
                (method === 'head' && vhost && this._lookup(path, vhost, 'get')) ||
                (method === 'head' && this._lookup(path, this.routes, 'get')) ||
                (method === 'options' && this.specials.options) ||
                (vhost && this._lookup(path, vhost, '*')) ||
                this._lookup(path, this.routes, '*') ||
                (this.specials.notFound || Boom.notFound());

    return route;
};

internals.Router.prototype._lookup = function (path, table, method) {

    var match = false;
    var routes = table[method];
    if (routes) {
        var pathSegments = path.split('/');
        for (var i = 0, il = routes.length; !match && i < il; ++i) {
            var record = routes[i];
            match = this._match(record, path, pathSegments);                    // Returns Error, false, or result object
            if (match) {
                if (match.isBoom) {
                    match = this.specials.badRequest || match;
                }
                else {
                    match.route = record.route;
                }
            }
        }
    }

    return match;
};

internals.Router.prototype._match = function (record, path, pathSegments) {

    var result = {
        params: {},
        paramsArray: []
    };

    // Literal comparison

    if (!record.params.length) {
        return (record.path === (this.settings.isCaseSensitive ? path : path.toLowerCase()) ? result : false);
    }

    // Mismatching segment count

    var pl = pathSegments.length - 1;
    var sl = record.segments.length;
    var last = record.segments[sl - 1];

    if (pl !== sl &&                                                        // Different count
        (pl !== sl - 1 || (!last.isEmptyOk && !last.isWildcard)) &&         // Not short one with empty or wildcard allowed
        (pl < sl || !last.isWildcard)) {

        return false;
    }

    // Parameter matching

    var match = true;
    for (var i = 0; match && (match instanceof Error === false) && i < record.segments.length; ++i) {
        var segment = record.segments[i];
        if (segment.isWildcard) {
            match = internals.setParam(segment.name, pathSegments.slice(i + 1).join('/'), result, true);
        }
        else if (segment.count) {
            match = internals.setParam(segment.name, pathSegments.slice(i + 1, i + 1 + segment.count).join('/'), result, false);
            i += (segment.count - 1);
        }
        else if (segment.name) {
            if (segment.extract) {
                var partial = pathSegments[i + 1].match(segment.extract);
                if (!partial) {
                    match = false;
                }
                else {
                    match = internals.setParam(segment.name, partial[1], result, segment.isEmptyOk);
                }
            }
            else {
                match = internals.setParam(segment.name, pathSegments[i + 1], result, segment.isEmptyOk);
            }
        }
        else {
            match = (segment.literal === (this.settings.isCaseSensitive ? pathSegments[i + 1] : pathSegments[i + 1].toLowerCase()));
        }
    }

    if (match !== true) {           // Can be Error
        return match;
    }

    return result;
};

internals.setParam = function (name, value, result, isEmptyOk) {

    if (!isEmptyOk && !value) {
        return false;
    }

    if (isEmptyOk && !value) {
        return true;
    }

    try {
        var decoded = decodeURIComponent(value);
        result.params[name] = decoded;
        result.paramsArray.push(decoded);
        return true;
    }
    catch (err) {
        return Boom.badRequest('Invalid request path');
    }
};

internals.generatePathRegex = function () {

    /*
        /path/{param}/path/{param?}
        /path/{param*2}/path
        /path/{param*2}
        /path/x{param}x
        /{param*}
    */

    var empty = '(^\\/$)';

    var legalChars = '[\\w\\!\\$&\'\\(\\)\\*\\+\\,;\\=\\:@\\-\\.~]';
    var encoded = '%[A-F0-9]{2}';

    var literalChar = '(?:' + legalChars + '|' + encoded + ')';
    var literal = literalChar + '+';
    var literalOptional = literalChar + '*';

    var midParam = '(\\{\\w+(\\*[1-9]\\d*)?\\})';                                   // {p}, {p*2}
    var endParam = '(\\/(\\{\\w+((\\*([1-9]\\d*)?)|(\\?))?\\})?)?';                 // {p}, {p*2}, {p*}, {p?}
    var mixParam = '(\\{\\w+\\??\\})';                                              // {p}, {p?}

    var literalParam = '(' + literal + mixParam + literalOptional + ')|(' + literalOptional + mixParam + literal + ')';

    var segmentContent = '(' + literal + '|' + midParam + '|' + literalParam + ')';
    var segment = '\\/' + segmentContent;
    var segments = '(' + segment + ')*';

    var path = '(^' + segments + endParam + '$)';

    var parseParam = '^(' + literalOptional + ')' + '\\{(\\w+)(?:(\\*)(\\d+)?)?(\\?)?\\}' + '(' + literalOptional + ')$';

    var expressions = {
        parseParam: new RegExp(parseParam),                                        // $1: literal-pre, $2: name, $3: *, $4: segments, $5: empty-ok, $6: literal-post
        validatePath: new RegExp(empty + '|' + path),
        validatePathEncoded: /%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g
    };

    return expressions;
};

internals.pathRegex = internals.generatePathRegex();

internals.Router.prototype.analyze = function (path) {

    Hoek.assert(internals.pathRegex.validatePath.test(path), 'Invalid path:', path);
    Hoek.assert(!internals.pathRegex.validatePathEncoded.test(path), 'Path cannot contain encoded non-reserved path characters:', path);

    var pathParts = path.split('/');
    var segments = [];
    var params = {};
    var fingers = [];

    for (var i = 1, il = pathParts.length; i < il; ++i) {                            // Skip first empty segment
        var segment = pathParts[i];
        var param = segment.match(internals.pathRegex.parseParam);
        if (param) {

            // Parameter

            var pre = param[1];
            var name = param[2];
            var isMulti = !!param[3];
            var multiCount = param[4] && parseInt(param[4], 10);
            var isEmptyOk = !!param[5];
            var post = param[6];

            Hoek.assert(!params[name], 'Cannot repeat the same parameter name:', name, 'in:', path);
            params[name] = true;

            if (isMulti) {
                if (multiCount) {
                    for (var m = 0; m < multiCount; ++m) {
                        fingers.push('?');
                        segments.push({ name: name, count: multiCount });
                    }
                }
                else {
                    fingers.push('#');
                    segments.push({ isWildcard: true, name: name });
                }
            }
            else {
                fingers.push(pre + '?' + post);
                var segmentMeta = {
                    name: name,
                    isEmptyOk: isEmptyOk
                };

                if (pre || post) {
                    segmentMeta.mixed = true;
                    segmentMeta.pre = pre;
                    segmentMeta.post = post;
                    segmentMeta.extract = new RegExp('^' + Hoek.escapeRegex(pre) + '(.' + (isEmptyOk ? '*' : '+') + ')' + Hoek.escapeRegex(post) + '$', (!this.settings.isCaseSensitive ? 'i' : ''));
                }

                segments.push(segmentMeta);
            }
        }
        else {

            // Literal

            segment = this.settings.isCaseSensitive ? segment : segment.toLowerCase();
            fingers.push(segment);
            segments.push({ literal: segment });
        }
    }

    return {
        segments: segments,
        fingerprint: '/' + fingers.join('/'),
        params: Object.keys(params)
    }
};

internals.Router.prototype.table = function (host) {

    var result = [];
    var collect = function (table) {

        if (!table) {
            return;
        }

        Object.keys(table).forEach(function (method) {

            table[method].forEach(function (record) {

                result.push(record.route);
            });
        });
    };

    if (this.vhosts) {
        var vhosts = host ? [].concat(host) : Object.keys(this.vhosts);
        for (var i = 0, il = vhosts.length; i < il; ++i) {
            collect(this.vhosts[vhosts[i]]);
        }
    }

    collect(this.routes);

    return result;
};

internals.sort = function (a, b) {

    // Biased for less and shorter segments which are faster to compare

    var aFirst = -1;
    var bFirst = 1;

    // Prepare fingerprints

    var aFingers = a.segments;
    var bFingers = b.segments;

    var al = aFingers.length;
    var bl = bFingers.length;

    // Compare fingerprints

    if ((aFingers[al - 1].isWildcard) ^ (bFingers[bl - 1].isWildcard)) {
        return (aFingers[al - 1].isWildcard ? bFirst : aFirst);
    }

    var size = Math.min(al, bl);
    for (var i = 0; i < size; ++i) {

        var aSegment = aFingers[i];
        var bSegment = bFingers[i];

        // Equal (except both wildcards as that means entire route paths are equal)

        if ((aSegment.name && bSegment.name && !aSegment.isWildcard && !bSegment.isWildcard && !aSegment.mixed && !bSegment.mixed) ||
            (aSegment.literal !== undefined && aSegment.literal === bSegment.literal)) {

            continue;
        }

        // One is wildcard

        if (aSegment.isWildcard) {
            return bFirst;
        }
        else if (bSegment.isWildcard) {
            return aFirst;
        }

        // One is parameter -or- both and at least one is mixed

        if (aSegment.name) {
            if (bSegment.name &&
                al === bl) {

                var mixed = internals.compareMixed(aSegment, bSegment);
                if (mixed === 0) {
                    continue;
                }

                return mixed;
            }
            else {
                return (al >= bl ? bFirst : aFirst);        // Bias to literal over parameter
            }
        }
        else if (bSegment.name) {
            break;
        }

        // Both literal but different

        if (al === bl) {
            if (aSegment.literal.length === bSegment.literal.length) {
                return (aSegment.literal > bSegment.literal ? bFirst : aFirst);
            }

            return (aSegment.literal.length > bSegment.literal.length ? bFirst : aFirst);
        }

        // Less segments win

        break;
    }

    return (al > bl ? bFirst : aFirst);
};

internals.compareMixed = function (aSegment, bSegment, p) {

    p = p || 0;
    if (p === 3) {
        return 0;
    }

    var aFirst = -1;
    var bFirst = 1;

    var prop = ['mixed', 'pre', 'post'][p];
    var ap = aSegment[prop];
    var bp = bSegment[prop];

    if (ap === bp) {
        return internals.compareMixed(aSegment, bSegment, p + 1);
    }
    else {
        if (ap) {
            if (bp) {
                if (ap.length === bp.length) {
                    return (ap > bp ? bFirst : aFirst);
                }
                else {
                    return (ap.length > bp.length ? aFirst : bFirst);
                }
            }
            else {
                return aFirst;
            }
        }
        else {
            return bFirst;
        }
    }
};<|endoftext|>


<commit_before>var annotationCache = {};

class LiftedArrow extends Arrow {
    constructor(f) {
        if (!(f instanceof Function)) {
            throw new Error('Cannot lift non-function');
        }

        super(_construct(() => {
            var start = window.performance.now();

            var s = f.toString();
            var i = s.indexOf('/*');
            var j = s.indexOf('*/', i + 1);
            var c = s.substring(i + 2, j);

            if (annotationCache[c] !== undefined) {
                var parsed = annotationCache[c];
            } else {
                var comment;
                try {
                    comment = c.match(/\@arrow :: (.*)\n?/)[1]
                } catch (err) {
                    if (typecheck) {
                        console.warn('Function being lifted does not contain an @arrow annotation');
                    }

                    comment = '_ ~> _';
                }

                try {
                    parsed = parser.parse(comment);
                } catch (err) {
                    throw new ComposeError(`Function being lifted does not contain a parseable @arrow annotation.\n${err.message}\n`);
                }

                annotationCache[c] = parsed;
            }

            var elapsed = window.performance.now() - start;
            numannotations++;
            annotationParseTime += elapsed;

            var arg = parsed[0];
            var out = parsed[1];
            var ncs = new ConstraintSet([]).addAll(parsed[2][0]);

            return new ArrowType(arg, out, ncs, parsed[2][1]).sanitize();
        }));

        this.f = f;
    }

    toString() {
        return 'lift :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        try {
            // If the function has more than one parameter and we have
            // an array argument, spread the elements. Else, just call
            // the function with a single argument.

            if (x && x.constructor === Array && this.f.length > 1) {
                var result = this.f.apply(null, x);
            } else {
                var result = this.f(x);
            }

            _check(this.type.out, result);
        } catch (err) {
            return h(err);
        }

        k(result);
    }

    equals(that) {
        return that instanceof LiftedArrow && this.f === that.f;
    }
}

class ElemArrow extends LiftedArrow {
    constructor(selector) {
        super(() => {
            /* @arrow :: _ ~> Elem */
            return $(selector);
        });

        this.selector = selector;
    }

    toString() {
        return 'elem :: ' + this.type.toString();
    }

    equals(that) {
        return that instanceof ElemArrow && this.selector === that.selector;
    }
}

//
// Simple Asynchronous Arrow Implementation
//

class SimpleAsyncArrow extends Arrow {
    isAsync() {
        return true;
    }
}

// Simple Asynchronous Arrow that takes in a config object

class SimpleConfigBasedAsyncArrow extends SimpleAsyncArrow {
    constructor(f, errorType) {
        super(_construct(() => {
            var start = window.performance.now();

            var s = f.toString();
            var i = s.indexOf('/*');
            var j = s.indexOf('*/', i + 1);
            var c = s.substring(i + 2, j);

            var ncs = new ConstraintSet([]);
            var err = [new NamedType(errorType)];

            if (annotationCache[c] !== undefined) {
                var conf = annotationCache[c][0];
                var resp = annotationCache[c][1];
            } else {
                try {
                    var conf = parser.parse(c.match(/\@conf :: (.*)\n?/)[1]);

                    ncs = ncs.addAll(conf[1][0]);
                    err = err.concat(conf[1][1]);
                } catch (err) {
                    throw new ComposeError(`Config does not contain a parseable @conf annotation.\n${err.message}\n`)
                }

                try {
                    var resp = parser.parse(c.match(/\@resp :: (.*)\n?/)[1]);

                    ncs = ncs.addAll(resp[1][0]);
                    err = err.concat(resp[1][1]);
                } catch (err) {
                    throw new ComposeError(`Config does not contain a parseable @resp annotation.\n${err.message}\n`)
                }

                annotationCache[c] = [conf, resp];
            }

            var elapsed = window.performance.now() - start;
            numannotations++;
            annotationParseTime += elapsed;

            return new ArrowType(conf[0], resp[0], ncs, err).sanitize();
        }));

        this.c = f;
    }
}

class AjaxArrow extends SimpleConfigBasedAsyncArrow {
    constructor(f) {
        super(f, 'AjaxError');
    }

    toString() {
        return 'ajax :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        // If the function has more than one parameter and we have
        // an array argument, spread the elements. Else, just call
        // the function with a single argument.

        // TODO - wrap this in try

        if (x && x.constructor === Array && this.c.length > 1) {
            var conf = this.c.apply(null, x);
        } else {
            var conf = this.c(x);
        }

        let abort = false;

        const cancel = () => {
            abort = true;
        }

        const fail = h;
        const succ = x => {
            _check(this.type.out, x);
            k(x);
        };

        $.ajax($.extend(conf, {
            success: (x, status, xhr) => { if (!abort) { p.advance(cancelerId); succ(x); } },
            error  : (xhr, status, x) => { if (!abort) { p.advance(cancelerId); fail(x); } },
        }));

        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        // TODO - deep comparison of objects
        return that instanceof AjaxArrow && this.c === that.c;
    }
}

class QueryArrow extends SimpleConfigBasedAsyncArrow {
    constructor(f, db) {
        super(f, 'QueryError');
        this.db = db;
    }

    toString() {
        return 'query :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        if (x && x.constructor === Array && this.c.length > 1) {
            var conf = this.c.apply(null, x);
        } else {
            var conf = this.c(x);
        }

        let abort = false;

        const cancel = () => {
            abort = true;
        };

        const fail = h;
        const succ = x => {
            _check(this.type.out, x);
            k(x);
        };

        this.db.query(conf.query, conf.param, function (err, rows) {
            if (err) {
                if (!abort) {
                    p.advance(cancelerId);
                    fail(err);
                }
            } else {
                if (!abort) {
                    p.advance(cancelerId);
                    succ(rows);
                }
            }
        });

        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        // TODO - deep comparison of objects
        return that instanceof QueryArrow && this.c === that.c;
    }
}

class EventArrow extends SimpleAsyncArrow {
    constructor(name) {
        // Elem ~> Event
        super(_construct(() => new ArrowType(new NamedType('Elem'), new NamedType('Event'))));
        this.name = name;
    }

    toString() {
        return 'event(' + this.name + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        let abort = false;

        const cancel = () => {
            abort = true;
            x.off(this.name, runner);
        };

        const runner = ev => {
            if (!abort) {
                cancel();
                p.advance(cancelerId);
                k(ev);
            }
        };

        x.on(this.name, runner);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof EventArrow && this.name === that.name;
    }
}

class DynamicDelayArrow extends SimpleAsyncArrow {
    constructor() {
        // Number ~> _
        super(_construct(() => {
            return new ArrowType(new NamedType('Number'), new TopType());
        }));
    }

    toString() {
        return 'delay :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        const cancel = () => clearTimeout(timer);
        const runner = () => {
            p.advance(cancelerId);
            k();
        };

        var timer = setTimeout(runner, x);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof DynamicDelayArrow;
    }
}

class DelayArrow extends SimpleAsyncArrow {
    constructor(duration) {
        // 'a ~> 'a
        super(_construct(() => {
            var alpha = ParamType.fresh();
            return new ArrowType(alpha, alpha);
        }));

        this.duration = duration;
    }

    toString() {
        return 'delay(' + this.duration + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        const cancel = () => clearTimeout(timer);
        const runner = () => {
            p.advance(cancelerId);
            k(x);
        };

        var timer = setTimeout(runner, this.duration);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof Delay && this.duration === that.duration;
    }
}

//
// Simple (Generalized) Arrows
//

class SplitArrow extends Arrow {
    constructor(n) {
        super(_construct(() => {
            var arg = ParamType.fresh();
            var out = Array.create(n, arg);

            return new ArrowType(arg, new TupleType(out));
        }));

        this.n = n;
    }

    toString() {
        return 'split(' + this.n + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        // TODO - clone values
        k(Array.create(this.n, x));
    }

    equals(that) {
        return that instanceof SplitArrow && this.n === that.n;
    }
}

class NthArrow extends Arrow {
    constructor(n) {
        super(_construct(() => {
            var arg = Array.create(n).map(() => ParamType.fresh());
            var out = arg[n - 1];

            return new ArrowType(new TupleType(arg), out);
        }));

        this.n = n;
    }

    toString() {
        return 'nth(' + this.n + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        k(x[this.n - 1]);
    }

    equals(that) {
        return that instanceof NthArrow && this.n === that.n;
    }
}
<commit_msg>Fix reference to parser after minification.<commit_after>var annotationCache = {};

class LiftedArrow extends Arrow {
    constructor(f) {
        if (!(f instanceof Function)) {
            throw new Error('Cannot lift non-function');
        }

        super(_construct(() => {
            var start = window.performance.now();

            var s = f.toString();
            var i = s.indexOf('/*');
            var j = s.indexOf('*/', i + 1);
            var c = s.substring(i + 2, j);

            if (annotationCache[c] !== undefined) {
                var parsed = annotationCache[c];
            } else {
                var comment;
                try {
                    comment = c.match(/\@arrow :: (.*)\n?/)[1]
                } catch (err) {
                    if (typecheck) {
                        console.warn('Function being lifted does not contain an @arrow annotation');
                    }

                    comment = '_ ~> _';
                }

                try {
                    // jison exports the parser name like this
                    parsed = arrows.parse(comment);
                } catch (err) {
                    throw new ComposeError(`Function being lifted does not contain a parseable @arrow annotation.\n${err.message}\n`);
                }

                annotationCache[c] = parsed;
            }

            var elapsed = window.performance.now() - start;
            numannotations++;
            annotationParseTime += elapsed;

            var arg = parsed[0];
            var out = parsed[1];
            var ncs = new ConstraintSet([]).addAll(parsed[2][0]);

            return new ArrowType(arg, out, ncs, parsed[2][1]).sanitize();
        }));

        this.f = f;
    }

    toString() {
        return 'lift :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        try {
            // If the function has more than one parameter and we have
            // an array argument, spread the elements. Else, just call
            // the function with a single argument.

            if (x && x.constructor === Array && this.f.length > 1) {
                var result = this.f.apply(null, x);
            } else {
                var result = this.f(x);
            }

            _check(this.type.out, result);
        } catch (err) {
            return h(err);
        }

        k(result);
    }

    equals(that) {
        return that instanceof LiftedArrow && this.f === that.f;
    }
}

class ElemArrow extends LiftedArrow {
    constructor(selector) {
        super(() => {
            /* @arrow :: _ ~> Elem */
            return $(selector);
        });

        this.selector = selector;
    }

    toString() {
        return 'elem :: ' + this.type.toString();
    }

    equals(that) {
        return that instanceof ElemArrow && this.selector === that.selector;
    }
}

//
// Simple Asynchronous Arrow Implementation
//

class SimpleAsyncArrow extends Arrow {
    isAsync() {
        return true;
    }
}

// Simple Asynchronous Arrow that takes in a config object

class SimpleConfigBasedAsyncArrow extends SimpleAsyncArrow {
    constructor(f, errorType) {
        super(_construct(() => {
            var start = window.performance.now();

            var s = f.toString();
            var i = s.indexOf('/*');
            var j = s.indexOf('*/', i + 1);
            var c = s.substring(i + 2, j);

            var ncs = new ConstraintSet([]);
            var err = [new NamedType(errorType)];

            if (annotationCache[c] !== undefined) {
                var conf = annotationCache[c][0];
                var resp = annotationCache[c][1];
            } else {
                try {
                    // jison exports the parser name like this
                    var conf = arrows.parse(c.match(/\@conf :: (.*)\n?/)[1]);

                    ncs = ncs.addAll(conf[1][0]);
                    err = err.concat(conf[1][1]);
                } catch (err) {
                    throw new ComposeError(`Config does not contain a parseable @conf annotation.\n${err.message}\n`)
                }

                try {
                    // jison exports the parser name like this
                    var resp = arrows.parse(c.match(/\@resp :: (.*)\n?/)[1]);

                    ncs = ncs.addAll(resp[1][0]);
                    err = err.concat(resp[1][1]);
                } catch (err) {
                    throw new ComposeError(`Config does not contain a parseable @resp annotation.\n${err.message}\n`)
                }

                annotationCache[c] = [conf, resp];
            }

            var elapsed = window.performance.now() - start;
            numannotations++;
            annotationParseTime += elapsed;

            return new ArrowType(conf[0], resp[0], ncs, err).sanitize();
        }));

        this.c = f;
    }
}

class AjaxArrow extends SimpleConfigBasedAsyncArrow {
    constructor(f) {
        super(f, 'AjaxError');
    }

    toString() {
        return 'ajax :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        // If the function has more than one parameter and we have
        // an array argument, spread the elements. Else, just call
        // the function with a single argument.

        // TODO - wrap this in try

        if (x && x.constructor === Array && this.c.length > 1) {
            var conf = this.c.apply(null, x);
        } else {
            var conf = this.c(x);
        }

        let abort = false;

        const cancel = () => {
            abort = true;
        }

        const fail = h;
        const succ = x => {
            _check(this.type.out, x);
            k(x);
        };

        $.ajax($.extend(conf, {
            success: (x, status, xhr) => { if (!abort) { p.advance(cancelerId); succ(x); } },
            error  : (xhr, status, x) => { if (!abort) { p.advance(cancelerId); fail(x); } },
        }));

        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        // TODO - deep comparison of objects
        return that instanceof AjaxArrow && this.c === that.c;
    }
}

class QueryArrow extends SimpleConfigBasedAsyncArrow {
    constructor(f, db) {
        super(f, 'QueryError');
        this.db = db;
    }

    toString() {
        return 'query :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        if (x && x.constructor === Array && this.c.length > 1) {
            var conf = this.c.apply(null, x);
        } else {
            var conf = this.c(x);
        }

        let abort = false;

        const cancel = () => {
            abort = true;
        };

        const fail = h;
        const succ = x => {
            _check(this.type.out, x);
            k(x);
        };

        this.db.query(conf.query, conf.param, function (err, rows) {
            if (err) {
                if (!abort) {
                    p.advance(cancelerId);
                    fail(err);
                }
            } else {
                if (!abort) {
                    p.advance(cancelerId);
                    succ(rows);
                }
            }
        });

        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        // TODO - deep comparison of objects
        return that instanceof QueryArrow && this.c === that.c;
    }
}

class EventArrow extends SimpleAsyncArrow {
    constructor(name) {
        // Elem ~> Event
        super(_construct(() => new ArrowType(new NamedType('Elem'), new NamedType('Event'))));
        this.name = name;
    }

    toString() {
        return 'event(' + this.name + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        let abort = false;

        const cancel = () => {
            abort = true;
            x.off(this.name, runner);
        };

        const runner = ev => {
            if (!abort) {
                cancel();
                p.advance(cancelerId);
                k(ev);
            }
        };

        x.on(this.name, runner);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof EventArrow && this.name === that.name;
    }
}

class DynamicDelayArrow extends SimpleAsyncArrow {
    constructor() {
        // Number ~> _
        super(_construct(() => {
            return new ArrowType(new NamedType('Number'), new TopType());
        }));
    }

    toString() {
        return 'delay :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        const cancel = () => clearTimeout(timer);
        const runner = () => {
            p.advance(cancelerId);
            k();
        };

        var timer = setTimeout(runner, x);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof DynamicDelayArrow;
    }
}

class DelayArrow extends SimpleAsyncArrow {
    constructor(duration) {
        // 'a ~> 'a
        super(_construct(() => {
            var alpha = ParamType.fresh();
            return new ArrowType(alpha, alpha);
        }));

        this.duration = duration;
    }

    toString() {
        return 'delay(' + this.duration + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        const cancel = () => clearTimeout(timer);
        const runner = () => {
            p.advance(cancelerId);
            k(x);
        };

        var timer = setTimeout(runner, this.duration);
        var cancelerId = p.addCanceler(cancel);
    }

    equals(that) {
        return that instanceof Delay && this.duration === that.duration;
    }
}

//
// Simple (Generalized) Arrows
//

class SplitArrow extends Arrow {
    constructor(n) {
        super(_construct(() => {
            var arg = ParamType.fresh();
            var out = Array.create(n, arg);

            return new ArrowType(arg, new TupleType(out));
        }));

        this.n = n;
    }

    toString() {
        return 'split(' + this.n + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        // TODO - clone values
        k(Array.create(this.n, x));
    }

    equals(that) {
        return that instanceof SplitArrow && this.n === that.n;
    }
}

class NthArrow extends Arrow {
    constructor(n) {
        super(_construct(() => {
            var arg = Array.create(n).map(() => ParamType.fresh());
            var out = arg[n - 1];

            return new ArrowType(new TupleType(arg), out);
        }));

        this.n = n;
    }

    toString() {
        return 'nth(' + this.n + ') :: ' + this.type.toString();
    }

    call(x, p, k, h) {
        k(x[this.n - 1]);
    }

    equals(that) {
        return that instanceof NthArrow && this.n === that.n;
    }
}
<|endoftext|>


<commit_before>let extend = require('extend')
let Helper = require('./helper')
let defaultRequestFn = getDefaultRequestFn()
let underscored = require('underscore.string/underscored')

class Wrapper {
  /**
   * Creates a new Wrapper given a client (function) and options object.
   * A function is required, over other values such as objects, so that
   * invocations may be proxied and trapped using apply.
   *
   * @constructor
   * @param {function} client
   * @param {object}   opts
   */
  constructor (client, opts) {
    this._client = client
    this._opts = opts
  }

  /**
   * Static factory method that, given an API URL and options object, returns
   * a new Proxy. An instance of the Wrapper class is applied as the handler
   * to a target function.
   *
   * @param   {string} url
   * @param   {object} opts
   * @returns {Proxy}
   */
  static create (url, opts) {
    let client = function () {}
    client._url = Helper.removeTrailingSlash(url)

    if (opts.whitelist) {
      let keys = opts.whitelist
      if (!(keys instanceof Array)) {
        keys = Object.keys(opts.whitelist)
      }

      keys.forEach((key) => {
        client[key] = function () {}
      })
    }

    let wrapper = new Wrapper(client, opts)

    let methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
    methods.forEach((method) => {
      client[method] = function () {
        return wrapper._invokeMethod(client, method.toUpperCase(), [...arguments])
      }
    })

    if (opts.aliases) {
      Object.keys(opts.aliases).forEach((key) => {
        let val = opts.aliases[key]
        client[key] = client[val].bind(client)
      })
    }

    return new Proxy(client, wrapper)
  }

  /**
   * Handles invocations outside of HTTP methods, e.g. client.users(1)
   *
   * @param   {function} client
   * @param   {function} context
   * @param   {*[]}      args
   * @returns {Proxy}
   */
  apply (client, context, args) {
    let str = args.join('/')
    let url = `${client._url}/${str}`
    return Wrapper.create(url, this._opts)
  }

  /**
   * Handles property accesses, e.g. client.users
   *
   * @param   {function} client
   * @param   {*}        name
   * @returns {Proxy|function|bool}
   * @throws  Will throw if a whitelist is supplied, and the property name
   *          is not in the list
   */
  get (client, name) {
    let whitelist = this._opts.whitelist

    let createUpdated = (str) => {
      let opts = Object.assign({}, this._opts)
      if (opts.camelCase) str = underscored(str)
      if (opts.whitelist) {
        opts.whitelist = opts.whitelist[str] || []
      }
      let url = `${client._url}/${str}`
      return Wrapper.create(url, opts)
    }

    if (typeof name === 'symbol') {
      // Likely inspecting the object in node where showProxy is false
      return true
    } else if (whitelist && this._inWhitelist(name)) {
      return createUpdated(name)
    } else if (name in client) {
      return client[name]
    } else if (whitelist) {
      let err = new Error(`${name} not listed in swaddle's whitelist`)
      throw err
    } else if (client === this._client) {
      return createUpdated(name)
    }
  }

  /**
   * Invokes the specified HTTP method with the supplied request library.
   * Handles modifications to the result, as well as response body.
   *
   * @param   {function} client
   * @param   {string}   method
   * @param   {*[]}      args
   * @returns {*}
   */
  _invokeMethod (client, method, args) {
    let part
    if (this._shouldWrapInBody(method, args)) {
      args[0] = {body: args[0]}
    } else if (this._isPartOfPath(args[0])) {
      part = args[0]
      args.shift()
    }

    if (!args[0] || typeof args[0] !== 'object') {
      args.unshift({})
    }

    let opts = args[0] = extend(true, {}, this._opts, args[0])
    opts.method = method

    let url = this._buildUrl(client._url, part, opts.extension)
    let returnBody = opts.returnBody
    let fn = opts.fn

    this._updateCamelCaseRequest(opts)
    this._updateJSONRequest(opts)

    let keys = ['returnBody', 'fn', 'whitelist', 'camelCase',
      'extension', 'aliases', 'sendAsBody']
    keys.forEach((key) => delete opts[key])

    args.unshift(url)

    if (returnBody && typeof args[args.length - 1] === 'function') {
      let cb = args[args.length - 1]
      args[args.length - 1] = (err, res) => cb(err, this._getBody(res))
    }

    let req = fn.apply(null, args)

    if (returnBody && req.then) {
      req = req.then(res => this._getBody(res))
    }

    return req
  }

  /**
   * Returns whether or not the string is in the whitelist.
   *
   * @param   {string} str
   * @returns {bool}
   */
  _inWhitelist (str) {
    let whitelist = this._opts.whitelist

    if (!whitelist) {
      return false
    } else if (whitelist instanceof Array) {
      return whitelist.indexOf(str) !== -1
    } else {
      return (str in whitelist)
    }
  }

  /**
   * Returns whether or not a value should be included in an URL's path.
   *
   * @param   {*}    x
   * @returns {bool}
   */
  _isPartOfPath (x) {
    return x != null && (!!x.substring || !!x.toFixed)
  }

  /**
   * Returns whether or not the data passed to the current invocation should
   * be nested in a body attribute, as specified by the sendAsBody option.
   *
   * @param   {string} method
   * @param   {*[]}    args
   * @returns {bool}
   */
  _shouldWrapInBody (method, args) {
    let methods = ['POST', 'PUT', 'PATCH']

    return args.length && this._opts.sendAsBody &&
      typeof args[0] !== 'function' && methods.indexOf(method) !== -1
  }

  /**
   * Returns the final request url, based on the current client url,
   * a part to append to the path, as well as an optional file extension.
   *
   * @param   {string} url
   * @param   {string} part
   * @param   {string} extensions
   * @returns {string}
   */
  _buildUrl(url, part, extension) {
    if (extension) {
      if (part == null) {
        url += `.${extension}`
      } else if (part.indexOf('?') === 0) {
        url += `.${extension}${part}`
      } else {
        url += `/${part}.${extension}`
      }
    } else if (part != null) {
      url += `/${part}`
    }
    return url
  }

  /**
   * If the camelCase option was enabled, renames all camelCase keys in the
   * request body object to be snake_case.
   *
   * @param {object} opts
   */
  _updateCamelCaseRequest (opts) {
    if (!opts.camelCase) return

    if (opts.json instanceof Object) {
      // request sets content-type to json when opts.json is an object
      return Helper.convertToSnakeCase(opts.json)
    } else if (!('body' in opts)) {
      return
    }

    Helper.convertToSnakeCase(opts.body)
  }

  /**
   * If the json option was enabled, serializes the request body and adds
   * a content-type header.
   *
   * @param {object} opts
   */
  _updateJSONRequest (opts) {
    if (!opts.json || !('body' in opts)) return

    if (opts.fn.name !== 'request') {
      // Need to stringify for got/fetch
      opts.body = JSON.stringify(opts.body)
    }

    // content-type needs to be set to json when using body
    opts.headers = opts.headers || {}
    opts.headers['content-type'] = 'application/json'
  }

  /**
   * Returns the body of the response. For fetch and fetch-compliant libraries,
   * returns a promise that resolves to the body. If the json opt is truthy,
   * the response body is parsed. If camelCase was true, it's also converted.
   *
   * @param   {*} res
   * @returns {*}
   */
  _getBody (res) {
    if (!res) return

    // Optionally normalize object keys
    let normalize = (res) => {
      return (this._opts.camelCase) ? Helper.convertToCamelCase(res) : res
    }

    // Handle whatwg spec fetch. Note that empty response body will cause
    // res.json() to throw, so use res.text() instead
    if (typeof res.text === 'function') {
      if (!this._opts.json) return res.text()
      return res.text().then((res) => {
        return res ? normalize(JSON.parse(res)) : res
      })
    }

    // Other request libraries, which serialize JSON automatically
    let body = this._isResponseObject(res) ? res.body : res
    return normalize(body)
  }

  /**
   * Returns whether or not an object is a response object, based on the
   * internal classes used by got, request, and fetch.
   *
   * @param   {*} res
   * @returns {bool}
   */
  _isResponseObject (res) {
    let hasReadableState = res._readableState instanceof Object &&
      res._readableState.constructor.name === 'ReadableState'
    let type = res.constructor.name

    return hasReadableState ||
      type === 'IncomingMessage' ||
      type.indexOf('Response') !== -1
  }
}

/**
 * Attempts to require and return request, got, or the window.fetch function,
 * in that order.
 *
 * @returns {function}
 */
function getDefaultRequestFn () {
  let libs = ['got', 'request-promise', 'request']
  for (let i = 0; i < libs.length; i++) {
    try {
      return require(libs[i])
    } catch (err) {
      // no-op
    }
  }

  if (typeof window !== 'undefined') {
    return window.fetch
  }
}

module.exports = function swaddle (url, opts = {}) {
  opts = opts || {}
  opts.fn = opts.fn || defaultRequestFn
  opts.headers = opts.headers || {}

  let defaultTrue = ['json', 'returnBody']
  defaultTrue.forEach((key) => {
    if (key in opts) return
    opts[key] = true
  })

  if (!url) {
    let err = new Error('swaddle requires an URL')
    throw err
  }

  if (opts.camelCase && (!opts.returnBody || !opts.json)) {
    let err = new Error('camelCase must be used with returnBody and json')
    throw err
  }

  if (!opts.fn) {
    let err = new Error(`swaddle requires one of the following:
      * request is installed (npm install --save request)
      * got is installed (npm install --save got)
      * that fetch be available in the browser
      * a request function is supplied via the "fn" option`)
    throw err
  }

  if (!opts.headers['User-Agent']) {
    opts.headers['User-Agent'] = 'swaddle'
  }

  return Wrapper.create(url, opts)
}
<commit_msg>Style fix<commit_after>let extend = require('extend')
let Helper = require('./helper')
let defaultRequestFn = getDefaultRequestFn()
let underscored = require('underscore.string/underscored')

class Wrapper {
  /**
   * Creates a new Wrapper given a client (function) and options object.
   * A function is required, over other values such as objects, so that
   * invocations may be proxied and trapped using apply.
   *
   * @constructor
   * @param {function} client
   * @param {object}   opts
   */
  constructor (client, opts) {
    this._client = client
    this._opts = opts
  }

  /**
   * Static factory method that, given an API URL and options object, returns
   * a new Proxy. An instance of the Wrapper class is applied as the handler
   * to a target function.
   *
   * @param   {string} url
   * @param   {object} opts
   * @returns {Proxy}
   */
  static create (url, opts) {
    let client = function () {}
    client._url = Helper.removeTrailingSlash(url)

    if (opts.whitelist) {
      let keys = opts.whitelist
      if (!(keys instanceof Array)) {
        keys = Object.keys(opts.whitelist)
      }

      keys.forEach((key) => {
        client[key] = function () {}
      })
    }

    let wrapper = new Wrapper(client, opts)

    let methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
    methods.forEach((method) => {
      client[method] = function () {
        return wrapper._invokeMethod(client, method.toUpperCase(), [...arguments])
      }
    })

    if (opts.aliases) {
      Object.keys(opts.aliases).forEach((key) => {
        let val = opts.aliases[key]
        client[key] = client[val].bind(client)
      })
    }

    return new Proxy(client, wrapper)
  }

  /**
   * Handles invocations outside of HTTP methods, e.g. client.users(1)
   *
   * @param   {function} client
   * @param   {function} context
   * @param   {*[]}      args
   * @returns {Proxy}
   */
  apply (client, context, args) {
    let str = args.join('/')
    let url = `${client._url}/${str}`
    return Wrapper.create(url, this._opts)
  }

  /**
   * Handles property accesses, e.g. client.users
   *
   * @param   {function} client
   * @param   {*}        name
   * @returns {Proxy|function|bool}
   * @throws  Will throw if a whitelist is supplied, and the property name
   *          is not in the list
   */
  get (client, name) {
    let whitelist = this._opts.whitelist

    let createUpdated = (str) => {
      let opts = Object.assign({}, this._opts)
      if (opts.camelCase) str = underscored(str)
      if (opts.whitelist) {
        opts.whitelist = opts.whitelist[str] || []
      }
      let url = `${client._url}/${str}`
      return Wrapper.create(url, opts)
    }

    if (typeof name === 'symbol') {
      // Likely inspecting the object in node where showProxy is false
      return true
    } else if (whitelist && this._inWhitelist(name)) {
      return createUpdated(name)
    } else if (name in client) {
      return client[name]
    } else if (whitelist) {
      let err = new Error(`${name} not listed in swaddle's whitelist`)
      throw err
    } else if (client === this._client) {
      return createUpdated(name)
    }
  }

  /**
   * Invokes the specified HTTP method with the supplied request library.
   * Handles modifications to the result, as well as response body.
   *
   * @param   {function} client
   * @param   {string}   method
   * @param   {*[]}      args
   * @returns {*}
   */
  _invokeMethod (client, method, args) {
    let part
    if (this._shouldWrapInBody(method, args)) {
      args[0] = {body: args[0]}
    } else if (this._isPartOfPath(args[0])) {
      part = args[0]
      args.shift()
    }

    if (!args[0] || typeof args[0] !== 'object') {
      args.unshift({})
    }

    let opts = args[0] = extend(true, {}, this._opts, args[0])
    opts.method = method

    let url = this._buildUrl(client._url, part, opts.extension)
    let returnBody = opts.returnBody
    let fn = opts.fn

    this._updateCamelCaseRequest(opts)
    this._updateJSONRequest(opts)

    let keys = ['returnBody', 'fn', 'whitelist', 'camelCase',
      'extension', 'aliases', 'sendAsBody']
    keys.forEach((key) => delete opts[key])

    args.unshift(url)

    if (returnBody && typeof args[args.length - 1] === 'function') {
      let cb = args[args.length - 1]
      args[args.length - 1] = (err, res) => cb(err, this._getBody(res))
    }

    let req = fn.apply(null, args)

    if (returnBody && req.then) {
      req = req.then(res => this._getBody(res))
    }

    return req
  }

  /**
   * Returns whether or not the string is in the whitelist.
   *
   * @param   {string} str
   * @returns {bool}
   */
  _inWhitelist (str) {
    let whitelist = this._opts.whitelist

    if (!whitelist) {
      return false
    } else if (whitelist instanceof Array) {
      return whitelist.indexOf(str) !== -1
    } else {
      return (str in whitelist)
    }
  }

  /**
   * Returns whether or not a value should be included in an URL's path.
   *
   * @param   {*}    x
   * @returns {bool}
   */
  _isPartOfPath (x) {
    return x != null && (!!x.substring || !!x.toFixed)
  }

  /**
   * Returns whether or not the data passed to the current invocation should
   * be nested in a body attribute, as specified by the sendAsBody option.
   *
   * @param   {string} method
   * @param   {*[]}    args
   * @returns {bool}
   */
  _shouldWrapInBody (method, args) {
    let methods = ['POST', 'PUT', 'PATCH']

    return args.length && this._opts.sendAsBody &&
      typeof args[0] !== 'function' && methods.indexOf(method) !== -1
  }

  /**
   * Returns the final request url, based on the current client url,
   * a part to append to the path, as well as an optional file extension.
   *
   * @param   {string} url
   * @param   {string} part
   * @param   {string} extensions
   * @returns {string}
   */
  _buildUrl (url, part, extension) {
    if (extension) {
      if (part == null) {
        url += `.${extension}`
      } else if (part.indexOf('?') === 0) {
        url += `.${extension}${part}`
      } else {
        url += `/${part}.${extension}`
      }
    } else if (part != null) {
      url += `/${part}`
    }
    return url
  }

  /**
   * If the camelCase option was enabled, renames all camelCase keys in the
   * request body object to be snake_case.
   *
   * @param {object} opts
   */
  _updateCamelCaseRequest (opts) {
    if (!opts.camelCase) return

    if (opts.json instanceof Object) {
      // request sets content-type to json when opts.json is an object
      return Helper.convertToSnakeCase(opts.json)
    } else if (!('body' in opts)) {
      return
    }

    Helper.convertToSnakeCase(opts.body)
  }

  /**
   * If the json option was enabled, serializes the request body and adds
   * a content-type header.
   *
   * @param {object} opts
   */
  _updateJSONRequest (opts) {
    if (!opts.json || !('body' in opts)) return

    if (opts.fn.name !== 'request') {
      // Need to stringify for got/fetch
      opts.body = JSON.stringify(opts.body)
    }

    // content-type needs to be set to json when using body
    opts.headers = opts.headers || {}
    opts.headers['content-type'] = 'application/json'
  }

  /**
   * Returns the body of the response. For fetch and fetch-compliant libraries,
   * returns a promise that resolves to the body. If the json opt is truthy,
   * the response body is parsed. If camelCase was true, it's also converted.
   *
   * @param   {*} res
   * @returns {*}
   */
  _getBody (res) {
    if (!res) return

    // Optionally normalize object keys
    let normalize = (res) => {
      return (this._opts.camelCase) ? Helper.convertToCamelCase(res) : res
    }

    // Handle whatwg spec fetch. Note that empty response body will cause
    // res.json() to throw, so use res.text() instead
    if (typeof res.text === 'function') {
      if (!this._opts.json) return res.text()
      return res.text().then((res) => {
        return res ? normalize(JSON.parse(res)) : res
      })
    }

    // Other request libraries, which serialize JSON automatically
    let body = this._isResponseObject(res) ? res.body : res
    return normalize(body)
  }

  /**
   * Returns whether or not an object is a response object, based on the
   * internal classes used by got, request, and fetch.
   *
   * @param   {*} res
   * @returns {bool}
   */
  _isResponseObject (res) {
    let hasReadableState = res._readableState instanceof Object &&
      res._readableState.constructor.name === 'ReadableState'
    let type = res.constructor.name

    return hasReadableState ||
      type === 'IncomingMessage' ||
      type.indexOf('Response') !== -1
  }
}

/**
 * Attempts to require and return request, got, or the window.fetch function,
 * in that order.
 *
 * @returns {function}
 */
function getDefaultRequestFn () {
  let libs = ['got', 'request-promise', 'request']
  for (let i = 0; i < libs.length; i++) {
    try {
      return require(libs[i])
    } catch (err) {
      // no-op
    }
  }

  if (typeof window !== 'undefined') {
    return window.fetch
  }
}

module.exports = function swaddle (url, opts = {}) {
  opts = opts || {}
  opts.fn = opts.fn || defaultRequestFn
  opts.headers = opts.headers || {}

  let defaultTrue = ['json', 'returnBody']
  defaultTrue.forEach((key) => {
    if (key in opts) return
    opts[key] = true
  })

  if (!url) {
    let err = new Error('swaddle requires an URL')
    throw err
  }

  if (opts.camelCase && (!opts.returnBody || !opts.json)) {
    let err = new Error('camelCase must be used with returnBody and json')
    throw err
  }

  if (!opts.fn) {
    let err = new Error(`swaddle requires one of the following:
      * request is installed (npm install --save request)
      * got is installed (npm install --save got)
      * that fetch be available in the browser
      * a request function is supplied via the "fn" option`)
    throw err
  }

  if (!opts.headers['User-Agent']) {
    opts.headers['User-Agent'] = 'swaddle'
  }

  return Wrapper.create(url, opts)
}
<|endoftext|>


<commit_before>// Copyright 2016 Google Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE
'use strict';

const median = require('median');
const lighthouse = require('lighthouse');
const ChromeLauncher = require('lighthouse/lighthouse-cli/chrome-launcher');
const perfConfig = require('lighthouse/lighthouse-core/config/perf.json');

const Sheets = require('./sheets/index');
const Chart = require('./chart/chart');
const metrics = require('./metrics');
const expectations = require('./expectations');
const {getMessage, getMessageWithPrefix} = require('./utils/messages');

const _MAX_LIGHTHOUSE_TRIES = 2;
const _SIGINT = 'SIGINT';
const _SIGINT_EXIT_CODE = 130;

class PWMetrics {

  constructor(url, opts) {
    this.url = url;
    this.flags = opts.flags || {};
    this.flags.disableCpuThrottling = this.flags.disableCpuThrottling || false;
    this.runs = this.flags.runs || 1;
    this.sheets = opts.sheets;
    this.expectations = opts.expectations || false;
    this.tryLighthouseCounter = 0;

    if (this.flags.expectations) {
      if (this.expectations) {
        expectations.validateMetrics(this.expectations);
        expectations.normalizeMetrics(this.expectations);
      } else throw new Error(getMessageWithPrefix('ERROR', 'NO_EXPECTATIONS_FOUND'));
    }
  }

  start() {
    const results = new Array(parseInt(this.runs, 10)).fill(false);

    // Kill spawned Chrome process in case of ctrl-C.
    process.on(_SIGINT, () => {
      if (this.launcher) {
        this.launcher.kill().then(() => process.exit(_SIGINT_EXIT_CODE), console.log(getMessage('CLOSING_CHROME')));
      }
    });

    // do our runs in sequence
    const allDone = results.reduce((chain, run, index) => {
      return chain.then(_ => {
        return this.run().then(data => {
          console.log(getMessageWithPrefix('SUCCESS', 'SUCCESS_RUN', index, results.length));
          results[index] = data;
        }).catch(error => {
          console.error(getMessageWithPrefix('ERROR', 'FAILED_RUN', index, results.length, error.message));
          throw new Error(error);
        });
      });
    }, Promise.resolve());

    return allDone.then(_ => {
      const ret = {runs: results};
      if (this.runs > 1 && !this.flags.submit) {
        ret.median = this.findMedianRun(results);
        console.log(getMessage('MEDIAN_RUN'));
        this.displayOutput(ret.median);
      } else if (this.flags.submit) {
        const sheets = new Sheets(this.sheets);
        return sheets.appendResults(ret.runs).then(_ => ret);
      }
      return ret;
    });
  }

  run() {
    return this.launchChrome()
      .then(() => {
        const lhOpts = Object.assign({}, this.flags); // pass along all CLI flags
        if (process.env.CI) {
          // handling CRI_TIMEOUT issue - https://github.com/GoogleChrome/lighthouse/issues/833
          this.tryLighthouseCounter = 0;
          return this.runLighthouseOnCI(this.url, lhOpts, perfConfig);
        } else {
          return lighthouse(this.url, lhOpts, perfConfig);
        }
      })
      .then(data => {
        return this.recordLighthouseTrace(data);
      })
      .then(data => {
        return this.launcher.kill().then(_ => data);
      })
      .catch(error => {
        throw new Error(error);
      });
  }

  runLighthouseOnCI(url, lhOpts, perfConfig) {
    return new Promise((resolve, reject) => {
      return lighthouse(url, lhOpts, perfConfig)
        .then(resolve)
        .catch(error => {
          if (error.code === 'CRI_TIMEOUT' && this.tryLighthouseCounter <= _MAX_LIGHTHOUSE_TRIES) {
            this.tryLighthouseCounter++;
            console.log(getMessage('CRI_TIMEOUT_RELAUNCH'));
            return this.runLighthouseOnCI().catch(error => {
              console.error(error.message);
              console.error(getMessage('CLOSING_CHROME'));
              return this.launcher.kill();
            });
          }
          if (this.tryLighthouseCounter > _MAX_LIGHTHOUSE_TRIES) {
            return reject(new Error(getMessage('CRI_TIMEOUT_REJECT')));
          }
        });
    });
  }

  launchChrome() {
    this.launcher = new (ChromeLauncher.ChromeLauncher || ChromeLauncher)();
    return this.launcher.isDebuggerReady()
      .catch(() => {
        console.log(getMessage('LAUNCHING_CHROME'));
        return this.launcher.run();
      });
  }

  recordLighthouseTrace(data) {
    try {
      const preparedData = metrics.prepareData(data);

      if (!this.flags.submit && this.runs <= 1) {
        this.displayOutput(preparedData);
      }

      if (this.flags.expectations) {
        expectations.checkExpectations(preparedData.timings, this.expectations);
      }

      return Promise.resolve(preparedData);
    } catch (error) {
      return Promise.reject(error);
    }
  }

  displayOutput(data) {
    if (this.flags.output) {
      return data;
    } else {
      this.showChart(data);
    }
    return data;
  }

  showChart(data) {
    // reverse to preserve the order, because cli-chart.
    let timings = data.timings;

    timings = timings.filter(r => {
      if (r.timing === undefined) {
        console.error(getMessageWithPrefix('ERROR', 'METRIC_IS_UNAVAILABLE', r.title));
      }
      // don't chart hidden metrics, but include in json
      return !metrics.hiddenMetrics.includes(r.id);
    });

    const fullWidthInMs = Math.max(...timings.map(result => result.timing));
    const maxLabelWidth = Math.max(...timings.map(result => result.title.length));

    const chartOps = {
      // 90% of terminal width to give some right margin
      width: process.stdout.columns * 0.9 - maxLabelWidth,
      xlabel: 'Time (ms) since navigation start',

      // nearest second
      maxBound: Math.ceil(fullWidthInMs / 1000) * 1000,
      xmax: fullWidthInMs,
      lmargin: maxLabelWidth + 1,

      // 2 rows per bar, horitzonal plot
      height: timings.length * 2,
      step: 2,
      direction: 'x'
    };

    const chart = new Chart(chartOps);
    timings.forEach(result => {
      chart.addBar({
        size: result.timing,
        label: result.title,
        barLabel: `${Math.floor(result.timing).toLocaleString()}`,
        color: result.color
      });
    });
    chart.draw();
    return data;
  }

  findMedianRun(results) {
    const ttiValues = results.map(r => r.timings.find(timing => timing.id === metrics.ids.TTI).timing);
    const medianTTI = median(ttiValues);
    // in the case of duplicate runs having the exact same TTI, we naively pick the first
    const medianRun = results.find(result => result.timings.find(timing => {
      return timing.id === metrics.ids.TTI && timing.timing === medianTTI;
    }));
    return medianRun;
  }
}

module.exports = PWMetrics;
<commit_msg>add todo comment for disableCpuThrottling<commit_after>// Copyright 2016 Google Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE
'use strict';

const median = require('median');
const lighthouse = require('lighthouse');
const ChromeLauncher = require('lighthouse/lighthouse-cli/chrome-launcher');
const perfConfig = require('lighthouse/lighthouse-core/config/perf.json');

const Sheets = require('./sheets/index');
const Chart = require('./chart/chart');
const metrics = require('./metrics');
const expectations = require('./expectations');
const {getMessage, getMessageWithPrefix} = require('./utils/messages');

const _MAX_LIGHTHOUSE_TRIES = 2;
const _SIGINT = 'SIGINT';
const _SIGINT_EXIT_CODE = 130;

class PWMetrics {

  constructor(url, opts) {
    this.url = url;
    this.flags = opts.flags || {};
    // @todo remove when new lighthouse version be released, because -https://github.com/GoogleChrome/lighthouse/pull/1778
    this.flags.disableCpuThrottling = this.flags.disableCpuThrottling || false;
    this.runs = this.flags.runs || 1;
    this.sheets = opts.sheets;
    this.expectations = opts.expectations || false;
    this.tryLighthouseCounter = 0;

    if (this.flags.expectations) {
      if (this.expectations) {
        expectations.validateMetrics(this.expectations);
        expectations.normalizeMetrics(this.expectations);
      } else throw new Error(getMessageWithPrefix('ERROR', 'NO_EXPECTATIONS_FOUND'));
    }
  }

  start() {
    const results = new Array(parseInt(this.runs, 10)).fill(false);

    // Kill spawned Chrome process in case of ctrl-C.
    process.on(_SIGINT, () => {
      if (this.launcher) {
        this.launcher.kill().then(() => process.exit(_SIGINT_EXIT_CODE), console.log(getMessage('CLOSING_CHROME')));
      }
    });

    // do our runs in sequence
    const allDone = results.reduce((chain, run, index) => {
      return chain.then(_ => {
        return this.run().then(data => {
          console.log(getMessageWithPrefix('SUCCESS', 'SUCCESS_RUN', index, results.length));
          results[index] = data;
        }).catch(error => {
          console.error(getMessageWithPrefix('ERROR', 'FAILED_RUN', index, results.length, error.message));
          throw new Error(error);
        });
      });
    }, Promise.resolve());

    return allDone.then(_ => {
      const ret = {runs: results};
      if (this.runs > 1 && !this.flags.submit) {
        ret.median = this.findMedianRun(results);
        console.log(getMessage('MEDIAN_RUN'));
        this.displayOutput(ret.median);
      } else if (this.flags.submit) {
        const sheets = new Sheets(this.sheets);
        return sheets.appendResults(ret.runs).then(_ => ret);
      }
      return ret;
    });
  }

  run() {
    return this.launchChrome()
      .then(() => {
        const lhOpts = Object.assign({}, this.flags); // pass along all CLI flags
        if (process.env.CI) {
          // handling CRI_TIMEOUT issue - https://github.com/GoogleChrome/lighthouse/issues/833
          this.tryLighthouseCounter = 0;
          return this.runLighthouseOnCI(this.url, lhOpts, perfConfig);
        } else {
          return lighthouse(this.url, lhOpts, perfConfig);
        }
      })
      .then(data => {
        return this.recordLighthouseTrace(data);
      })
      .then(data => {
        return this.launcher.kill().then(_ => data);
      })
      .catch(error => {
        throw new Error(error);
      });
  }

  runLighthouseOnCI(url, lhOpts, perfConfig) {
    return new Promise((resolve, reject) => {
      return lighthouse(url, lhOpts, perfConfig)
        .then(resolve)
        .catch(error => {
          if (error.code === 'CRI_TIMEOUT' && this.tryLighthouseCounter <= _MAX_LIGHTHOUSE_TRIES) {
            this.tryLighthouseCounter++;
            console.log(getMessage('CRI_TIMEOUT_RELAUNCH'));
            return this.runLighthouseOnCI().catch(error => {
              console.error(error.message);
              console.error(getMessage('CLOSING_CHROME'));
              return this.launcher.kill();
            });
          }
          if (this.tryLighthouseCounter > _MAX_LIGHTHOUSE_TRIES) {
            return reject(new Error(getMessage('CRI_TIMEOUT_REJECT')));
          }
        });
    });
  }

  launchChrome() {
    this.launcher = new (ChromeLauncher.ChromeLauncher || ChromeLauncher)();
    return this.launcher.isDebuggerReady()
      .catch(() => {
        console.log(getMessage('LAUNCHING_CHROME'));
        return this.launcher.run();
      });
  }

  recordLighthouseTrace(data) {
    try {
      const preparedData = metrics.prepareData(data);

      if (!this.flags.submit && this.runs <= 1) {
        this.displayOutput(preparedData);
      }

      if (this.flags.expectations) {
        expectations.checkExpectations(preparedData.timings, this.expectations);
      }

      return Promise.resolve(preparedData);
    } catch (error) {
      return Promise.reject(error);
    }
  }

  displayOutput(data) {
    if (this.flags.output) {
      return data;
    } else {
      this.showChart(data);
    }
    return data;
  }

  showChart(data) {
    // reverse to preserve the order, because cli-chart.
    let timings = data.timings;

    timings = timings.filter(r => {
      if (r.timing === undefined) {
        console.error(getMessageWithPrefix('ERROR', 'METRIC_IS_UNAVAILABLE', r.title));
      }
      // don't chart hidden metrics, but include in json
      return !metrics.hiddenMetrics.includes(r.id);
    });

    const fullWidthInMs = Math.max(...timings.map(result => result.timing));
    const maxLabelWidth = Math.max(...timings.map(result => result.title.length));

    const chartOps = {
      // 90% of terminal width to give some right margin
      width: process.stdout.columns * 0.9 - maxLabelWidth,
      xlabel: 'Time (ms) since navigation start',

      // nearest second
      maxBound: Math.ceil(fullWidthInMs / 1000) * 1000,
      xmax: fullWidthInMs,
      lmargin: maxLabelWidth + 1,

      // 2 rows per bar, horitzonal plot
      height: timings.length * 2,
      step: 2,
      direction: 'x'
    };

    const chart = new Chart(chartOps);
    timings.forEach(result => {
      chart.addBar({
        size: result.timing,
        label: result.title,
        barLabel: `${Math.floor(result.timing).toLocaleString()}`,
        color: result.color
      });
    });
    chart.draw();
    return data;
  }

  findMedianRun(results) {
    const ttiValues = results.map(r => r.timings.find(timing => timing.id === metrics.ids.TTI).timing);
    const medianTTI = median(ttiValues);
    // in the case of duplicate runs having the exact same TTI, we naively pick the first
    const medianRun = results.find(result => result.timings.find(timing => {
      return timing.id === metrics.ids.TTI && timing.timing === medianTTI;
    }));
    return medianRun;
  }
}

module.exports = PWMetrics;
<|endoftext|>