UNPKG

scientist

Version:

Carefully refactor critical paths in production

209 lines (182 loc) 6.19 kB
var EventEmitter, Experiment, Observation, Promise, Result, _, expects, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; _ = require('underscore'); EventEmitter = require('events').EventEmitter; Promise = require('bluebird'); Observation = require('./observation'); Result = require('./result'); expects = function(type, wrapped) { return function(arg) { if (typeof arg !== type) { throw TypeError("Expected " + type + ", got " + arg); } return wrapped.call(this, arg); }; }; Experiment = (function(superClass) { extend(Experiment, superClass); function Experiment(name) { Experiment.__super__.constructor.call(this); this.name = name; this._behaviors = {}; this._options = { context: {}, async: false, skipper: _.constant(false), mapper: _.identity, ignorers: [], comparator: _.isEqual, cleaner: _.identity }; } Experiment.prototype.use = function(block) { return this["try"]('control', block); }; Experiment.prototype["try"] = function() { var arg1, block, i, name; arg1 = 2 <= arguments.length ? slice.call(arguments, 0, i = arguments.length - 1) : (i = 0, []), block = arguments[i++]; name = arg1[0]; if (name == null) { name = 'candidate'; } if (name in this._behaviors) { throw Error("Duplicate behavior: " + name); } if (!_.isFunction(block)) { throw TypeError("Invalid block: expected function, got " + block); } return this._behaviors[name] = block; }; Experiment.prototype.run = function(sampler) { var candidates, control, hasNoBehaviors, observations, shouldNotSample, shouldSkip, skipReason; if (!('control' in this._behaviors)) { throw Error("Expected control behavior to be defined"); } hasNoBehaviors = _.size(this._behaviors) < 2; shouldNotSample = this._try("Sampler", (function(_this) { return function() { return !sampler(_this.name); }; })(this)); shouldSkip = this._try("Skipper", (function(_this) { return function() { return _this._options.skipper(); }; })(this)); skipReason = (function() { switch (false) { case !hasNoBehaviors: return "No behaviors defined"; case !shouldNotSample: return "Sampler returned false"; case !shouldSkip: return "Skipper returned true"; } })(); if (skipReason) { this._try("Skip handler", (function(_this) { return function() { return _this.emit('skip', _this, skipReason); }; })(this)); return this._behaviors.control(); } observations = _(this._behaviors).chain().keys().shuffle().map((function(_this) { return function(key) { return new Observation(key, _this._behaviors[key], _this._options); }; })(this)).value(); control = _.find(observations, { name: 'control' }); candidates = _.without(observations, control); this._sendResults([control].concat(candidates)); return control.evaluation(); }; Experiment.prototype._sendResults = function(observations) { var mapped; mapped = this._try("Map", (function(_this) { return function() { return _.invoke(observations, 'map', _this._mapper.bind(_this)); }; })(this)); if (!mapped) { return; } return Promise.map(mapped, this._settle.bind(this)).spread((function(_this) { return function() { var candidates, control, result; control = arguments[0], candidates = 2 <= arguments.length ? slice.call(arguments, 1) : []; result = _this._try("Comparison", function() { return new Result(_this, control, candidates); }); if (!result) { return; } return _this._try("Result handler", function() { return _this.emit('result', result); }); }; })(this)).done(); }; Experiment.prototype.context = function(context) { return _.extend(this._options.context, context); }; Experiment.prototype.async = expects('boolean', function(async) { return this._options.async = async; }); Experiment.prototype.skipWhen = expects('function', function(skipper) { return this._options.skipper = skipper; }); Experiment.prototype.map = expects('function', function(mapper) { return this._options.mapper = mapper; }); Experiment.prototype.ignore = expects('function', function(ignorer) { return this._options.ignorers.push(ignorer); }); Experiment.prototype.compare = expects('function', function(comparator) { return this._options.comparator = comparator; }); Experiment.prototype.clean = expects('function', function(cleaner) { return this._options.cleaner = cleaner; }); Experiment.prototype._try = function(operation, block) { var err, error; try { return block(); } catch (error) { err = error; this.emit('error', this._decorateError(err, operation + " failed")); return null; } }; Experiment.prototype._settle = function(observation) { if (this._options.async) { return observation.settle(); } else { return observation; } }; Experiment.prototype._mapper = function(val) { var result; if (this._options.async) { result = this._options.mapper(Promise.resolve(val)); if (!_.isFunction(result != null ? result.then : void 0)) { throw Error("Result of async mapping must be a thenable, got " + result); } return result; } else { return this._options.mapper(val); } }; Experiment.prototype._decorateError = function(err, prefix) { err.message = prefix + ": " + err.message; err.experiment = this; err.context = this.context(); return err; }; return Experiment; })(EventEmitter); module.exports = Experiment;