| Line | Hits | Source |
|---|---|---|
| 1 | 1 | var |
| 2 | _ = require('lodash'), | |
| 3 | assert = require('assert'), | |
| 4 | events = require('events'), | |
| 5 | http = require('http'), | |
| 6 | P = require('bluebird'), | |
| 7 | util = require('util'), | |
| 8 | Feature = require('./lib/feature'), | |
| 9 | FileAdapter = require('./lib/file'), | |
| 10 | RedisAdapter = require('./lib/redis') | |
| 11 | ; | |
| 12 | ||
| 13 | 1 | var Reflip = module.exports = function(opts) |
| 14 | { | |
| 15 | 9 | events.EventEmitter.call(this); |
| 16 | ||
| 17 | 9 | assert(opts, 'You must pass options to the Reflip constructor'); |
| 18 | 9 | assert(opts.storage || opts.features, 'You must pass either storage options or a pre-set feature object'); |
| 19 | ||
| 20 | 10 | if (opts.features) this.features = opts.features; |
| 21 | 9 | if (_.has(opts, 'default')) this.default = opts.default; |
| 22 | 9 | if (opts.httpcode) this.httpcode = opts.httpcode; |
| 23 | 9 | this.exportName = opts.exportName || 'check'; |
| 24 | ||
| 25 | 9 | this.features = {}; |
| 26 | ||
| 27 | 9 | if (opts.storage) |
| 28 | { | |
| 29 | 8 | var self = this; |
| 30 | 8 | this.storage = opts.storage; |
| 31 | 17 | this.storage.on('update', function(f) { self.update(f); }); |
| 32 | 8 | this.refresh(); |
| 33 | } | |
| 34 | }; | |
| 35 | 1 | util.inherits(Reflip, events.EventEmitter); |
| 36 | ||
| 37 | 1 | Reflip.FileAdapter = FileAdapter; |
| 38 | 1 | Reflip.RedisAdapter = RedisAdapter; |
| 39 | 1 | Reflip.Feature = Feature; |
| 40 | ||
| 41 | 1 | Reflip.prototype.storage = null; |
| 42 | 1 | Reflip.prototype.features = null; |
| 43 | 1 | Reflip.prototype.default = false; |
| 44 | 1 | Reflip.prototype.httpcode = 404; |
| 45 | ||
| 46 | 1 | Reflip.prototype.flip = function() |
| 47 | { | |
| 48 | 4 | var self = this; |
| 49 | 4 | var defEnabled = this.default; |
| 50 | ||
| 51 | 4 | function middleware(request, response, next) |
| 52 | { | |
| 53 | 3 | request[self.exportName] = function(name) |
| 54 | { | |
| 55 | 3 | if (name == null) |
| 56 | { | |
| 57 | 1 | return _.transform(self.features, function(features, v, k) |
| 58 | { | |
| 59 | 4 | features[k] = v.check(request); |
| 60 | }); | |
| 61 | } | |
| 62 | ||
| 63 | 2 | if (!_.has(self.features, name)) |
| 64 | 1 | return self.default; |
| 65 | ||
| 66 | 1 | return self.features[name].check(request); |
| 67 | }; | |
| 68 | 3 | next(); |
| 69 | } | |
| 70 | ||
| 71 | 4 | return middleware; |
| 72 | }; | |
| 73 | ||
| 74 | 1 | Reflip.prototype.gate = function gate(name, failureHandler) |
| 75 | { | |
| 76 | 5 | var self = this; |
| 77 | 5 | assert(name && name.length, 'You must provide a feature name'); |
| 78 | ||
| 79 | 4 | var isCustomResponse = typeof failureHandler == 'function'; |
| 80 | 4 | function gateFunc(request, response, next) |
| 81 | { | |
| 82 | 3 | if (request.check(name)) |
| 83 | 1 | return next(); |
| 84 | ||
| 85 | 2 | if (!isCustomResponse) |
| 86 | 1 | return response.send(self.httpcode, http.STATUS_CODES[self.httpcode]); |
| 87 | ||
| 88 | 1 | failureHandler(request, response); |
| 89 | } | |
| 90 | ||
| 91 | 4 | return gateFunc; |
| 92 | }; | |
| 93 | ||
| 94 | 1 | Reflip.prototype.register = function register(name, func) |
| 95 | { | |
| 96 | 3 | var feat; |
| 97 | 3 | if (name instanceof Feature) |
| 98 | 1 | feat = name; |
| 99 | else | |
| 100 | { | |
| 101 | 2 | feat = new Feature( |
| 102 | { | |
| 103 | name: name, | |
| 104 | type: 'custom', | |
| 105 | enabled: true, | |
| 106 | checker: func | |
| 107 | }); | |
| 108 | } | |
| 109 | ||
| 110 | 3 | this.features[feat.name] = feat; |
| 111 | }; | |
| 112 | ||
| 113 | 1 | Reflip.prototype.refresh = function refresh() |
| 114 | { | |
| 115 | 8 | if (!this.storage) |
| 116 | 0 | return P.cast(true); |
| 117 | ||
| 118 | 8 | var self = this; |
| 119 | 8 | this.emit('refreshing'); |
| 120 | 8 | self.storage.refresh(); |
| 121 | }; | |
| 122 | ||
| 123 | 1 | Reflip.prototype.update = function update(features) |
| 124 | { | |
| 125 | 9 | var self = this; |
| 126 | ||
| 127 | 9 | _.each(features, function(def) |
| 128 | { | |
| 129 | 35 | var feature = new Feature(def); |
| 130 | 35 | self.features[feature.name] = feature; |
| 131 | }); | |
| 132 | 9 | self.emit('ready'); |
| 133 | }; | |
| 134 | ||
| 135 | 1 | Reflip.prototype.shutdown = function shutdown() |
| 136 | { | |
| 137 | 2 | if (this.storage) |
| 138 | 2 | this.storage.close(); |
| 139 | }; | |
| 140 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | var |
| 2 | _ = require('lodash'), | |
| 3 | assert = require('assert') | |
| 4 | ; | |
| 5 | ||
| 6 | 1 | var Feature = module.exports = function Feature(opts) |
| 7 | { | |
| 8 | 38 | assert(opts && (typeof opts == 'object'), 'you must pass an options object to the Feature constructor'); |
| 9 | ||
| 10 | 38 | this.name = opts.name; |
| 11 | 38 | this.type = opts.type; |
| 12 | 38 | this.enabled = opts.enabled; |
| 13 | 38 | this.chance = _.has(opts, 'chance') ? opts.chance : 100; |
| 14 | 38 | this.checker = opts.checker; |
| 15 | 38 | this.groups = opts.groups; |
| 16 | }; | |
| 17 | ||
| 18 | 1 | Feature.prototype.name = 'aardvarks'; // name |
| 19 | 1 | Feature.prototype.type = 'boolean'; // one of boolean, grouped, metered, custom |
| 20 | 1 | Feature.prototype.enabled = false; // on/off by default |
| 21 | 1 | Feature.prototype.chance = 100; // percentage; consulted if this is a metered feature |
| 22 | 1 | Feature.prototype.checker = null; // user-supplied check function |
| 23 | 1 | Feature.prototype.groups = null; // used for a/b testing stuff |
| 24 | ||
| 25 | 1 | Feature.prototype.check = function check(request) |
| 26 | { | |
| 27 | 8 | if (!this.enabled) |
| 28 | 3 | return false; |
| 29 | ||
| 30 | 5 | switch (this.type) |
| 31 | { | |
| 32 | case 'custom': | |
| 33 | 1 | return this.checker(request); |
| 34 | ||
| 35 | case 'boolean': | |
| 36 | 2 | return true; |
| 37 | ||
| 38 | case 'metered': | |
| 39 | 1 | return (Math.random() * 100 > this.chance); |
| 40 | ||
| 41 | case 'grouped': | |
| 42 | 1 | return this.checkGroup(request); |
| 43 | } | |
| 44 | }; | |
| 45 | ||
| 46 | 1 | Feature.prototype.checkGroup = function checkGroup(request) |
| 47 | { | |
| 48 | 1 | var idx = _.random(0, this.groups.length - 1); |
| 49 | 1 | return this.groups[idx]; |
| 50 | ||
| 51 | // TODO store it in the session? | |
| 52 | }; | |
| 53 | ||
| 54 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | var |
| 2 | assert = require('assert'), | |
| 3 | events = require('events'), | |
| 4 | fs = require('fs'), | |
| 5 | P = require('bluebird'), | |
| 6 | path = require('path'), | |
| 7 | util = require('util') | |
| 8 | ; | |
| 9 | ||
| 10 | 1 | var FileAdapter = module.exports = function FileAdapter(opts) |
| 11 | { | |
| 12 | 12 | assert(opts && (typeof opts == 'object'), 'you must pass an options object to the RedisAdapter constructor'); |
| 13 | 11 | assert(opts.filename, 'you must specify a file to read from'); |
| 14 | ||
| 15 | 10 | this.filename = opts.filename; |
| 16 | ||
| 17 | 10 | var self = this; |
| 18 | 10 | this.watcher = fs.watch(this.filename, function(event, fname) |
| 19 | { | |
| 20 | 2 | self.refresh(); |
| 21 | }); | |
| 22 | }; | |
| 23 | 1 | util.inherits(FileAdapter, events.EventEmitter); |
| 24 | ||
| 25 | 1 | FileAdapter.prototype.filename = null; |
| 26 | ||
| 27 | 1 | FileAdapter.prototype.refresh = function refresh() |
| 28 | { | |
| 29 | 8 | var self = this; |
| 30 | ||
| 31 | 8 | this.read() |
| 32 | .then(function(response) | |
| 33 | { | |
| 34 | 8 | self.emit('update', response.features); |
| 35 | }, | |
| 36 | function(err) | |
| 37 | { | |
| 38 | 0 | self.emit('error', err); |
| 39 | }); | |
| 40 | }; | |
| 41 | ||
| 42 | 1 | FileAdapter.prototype.read = function read() |
| 43 | { | |
| 44 | 11 | var deferred = P.pending(); |
| 45 | ||
| 46 | 11 | fs.readFile(this.filename, function(err, data) |
| 47 | { | |
| 48 | 11 | if (err) return deferred.reject(err); |
| 49 | 11 | parseJSON(data, function(err, value) |
| 50 | { | |
| 51 | 11 | if (err) return deferred.reject(err); |
| 52 | 11 | deferred.fulfill(value); |
| 53 | }); | |
| 54 | }); | |
| 55 | ||
| 56 | 11 | return deferred.promise; |
| 57 | }; | |
| 58 | ||
| 59 | 1 | FileAdapter.prototype.close = function close() |
| 60 | { | |
| 61 | 3 | this.watcher.close(); |
| 62 | }; | |
| 63 | ||
| 64 | 1 | function parseJSON(data, callback) |
| 65 | { | |
| 66 | 11 | var value; |
| 67 | 11 | try |
| 68 | { | |
| 69 | 11 | value = JSON.parse(data); |
| 70 | } | |
| 71 | catch (err) | |
| 72 | { | |
| 73 | 0 | return callback(err); |
| 74 | } | |
| 75 | 11 | callback(null, value); |
| 76 | } | |
| 77 |
| Line | Hits | Source |
|---|---|---|
| 1 | 1 | var |
| 2 | _ = require('lodash'), | |
| 3 | assert = require('assert'), | |
| 4 | events = require('events'), | |
| 5 | P = require('bluebird'), | |
| 6 | redis = require('redis'), | |
| 7 | util = require('util') | |
| 8 | ; | |
| 9 | ||
| 10 | 1 | var RedisAdapter = module.exports = function RedisAdapter(opts) |
| 11 | { | |
| 12 | 9 | assert(opts && (typeof opts == 'object'), 'you must pass an options object to the RedisAdapter constructor'); |
| 13 | 8 | assert(opts.client || (opts.host && opts.port), 'you must pass either a redis client or options sufficient to create one'); |
| 14 | ||
| 15 | 7 | if (opts.ttl) this.ttl = opts.ttl; |
| 16 | 9 | if (opts.namespace) this.namespace = opts.namespace; |
| 17 | ||
| 18 | 7 | if (opts.client) |
| 19 | 6 | this.client = opts.client; |
| 20 | else | |
| 21 | 1 | this.client = redis.createClient(opts.port, opts.host); |
| 22 | ||
| 23 | }; | |
| 24 | 1 | util.inherits(RedisAdapter, events.EventEmitter); |
| 25 | ||
| 26 | 1 | RedisAdapter.prototype.client = null; |
| 27 | 1 | RedisAdapter.prototype.namespace = 'reflip:'; |
| 28 | 1 | RedisAdapter.prototype.ttl = 5 * 60 * 1000; // 5 minutes |
| 29 | 1 | RedisAdapter.prototype.refreshTimer = null; |
| 30 | ||
| 31 | 1 | RedisAdapter.prototype.makeKey = function makeKey(base) |
| 32 | { | |
| 33 | 21 | return this.namespace + base; |
| 34 | }; | |
| 35 | ||
| 36 | 1 | RedisAdapter.prototype.refresh = function refresh() |
| 37 | { | |
| 38 | 2 | var self = this; |
| 39 | ||
| 40 | 2 | self.read() |
| 41 | .then(function(response) | |
| 42 | { | |
| 43 | 2 | self.emit('update', response.features); |
| 44 | }, function(err) | |
| 45 | { | |
| 46 | 0 | console.log(err); |
| 47 | }); | |
| 48 | }; | |
| 49 | ||
| 50 | 1 | RedisAdapter.prototype.read = function read() |
| 51 | { | |
| 52 | 4 | var self = this, |
| 53 | deferred = P.pending(), | |
| 54 | result = { features: [] }; | |
| 55 | ||
| 56 | 4 | var chain = self.client.multi(); |
| 57 | 4 | chain.smembers(self.makeKey('features')); |
| 58 | 4 | chain.get(self.makeKey('ttl')); |
| 59 | ||
| 60 | 4 | chain.exec(function(err, replies) |
| 61 | { | |
| 62 | 4 | if (err) return deferred.reject(err); |
| 63 | ||
| 64 | 4 | if (!_.isNull(replies[1])) |
| 65 | 3 | result.ttl = parseInt(replies[1], 10); |
| 66 | else | |
| 67 | 1 | result.ttl = 0; |
| 68 | 4 | self.ttl = result.ttl; |
| 69 | ||
| 70 | 4 | if (self.ttl) |
| 71 | 3 | self.refreshTimer = setInterval(self.refresh.bind(self), self.ttl); |
| 72 | else | |
| 73 | 1 | self.refreshTimer = null; |
| 74 | ||
| 75 | 4 | var chain2 = self.client.multi(); |
| 76 | 4 | _.each(replies[0], function(f) |
| 77 | { | |
| 78 | 12 | chain2.hgetall(self.makeKey(f)); |
| 79 | }); | |
| 80 | ||
| 81 | 4 | chain2.exec(function(err, hashes) |
| 82 | { | |
| 83 | 4 | result.features = hashes; |
| 84 | 4 | deferred.fulfill(result); |
| 85 | }); | |
| 86 | }); | |
| 87 | ||
| 88 | 4 | return deferred.promise; |
| 89 | }; | |
| 90 | ||
| 91 | 1 | RedisAdapter.prototype.close = function close() |
| 92 | { | |
| 93 | 0 | this.client.close(); |
| 94 | 0 | this.client = null; |
| 95 | 0 | if (this.refreshTimer) |
| 96 | { | |
| 97 | 0 | clearInterval(this.refreshTimer); |
| 98 | 0 | this.refreshTimer = null; |
| 99 | } | |
| 100 | }; | |
| 101 |