Last active
December 19, 2015 10:49
-
-
Save optilude/5943423 to your computer and use it in GitHub Desktop.
Generic, asynchronous, dependency-aware, promise-based operations registry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*jshint globalstrict:true, devel:true */ | |
/*global require, module, exports, process, __dirname */ | |
"use strict"; | |
var Toposort = require('toposort-class'), | |
promise = require('node-promise'), | |
_ = require('underscore'); | |
module.exports = (function() { | |
var CalculatorRegistry = function() { | |
this.calculators = {}; // name -> function | |
this.dependencies = {}; // name -> list of dependencies | |
}; | |
/** | |
* Register a calculator function with a name and a list of names of | |
* calculators that should run before this one. | |
* | |
* The calculator function should have this signature: | |
* | |
* var promise = require('node-promise'); // Or another Promises/A implementation | |
* | |
* function calculateSomething(portfolio, promises) { | |
* var deferred = promise.defer(); | |
* | |
* // Do something to calculate results, possibly asynchronously | |
* performCalculation() | |
* .then(function(results) { | |
* deferred.resolve(results) | |
* }); | |
* | |
* return deferred.promise; | |
* } | |
* | |
* The `portfolio` argument is a portfolio object with reference data | |
* loaded. | |
* | |
* The `promises` argument is an object with string keys representing | |
* calculators that have already been started, and promises as values. | |
* If this calculator declares any dependencies, then the `promises` | |
* object is guaranteed to contain promises for at least those dependencies. | |
* In other words, you can use `promises['some-calculation'].then(...)` to | |
* get ther results of a preceding calculation when available. | |
* | |
* For this all to work, of course, each calculator must return a promise. | |
* | |
*/ | |
CalculatorRegistry.prototype.add = function(name, dependencies, fn) { | |
this.calculators[name] = fn; | |
this.dependencies[name] = dependencies; | |
}; | |
/** | |
* Run the calculator with the given name and any dependencies. Returns a | |
* promise, which will be resolved with a result set once all calculators | |
* are complete. The result set is an object keyed on calcualtor naeme, so | |
* `resultSet['some-calculator']` contains the results of the the calculator | |
* called 'some-calculator'. (There may be more than one if the calculator | |
* has dependencies.) | |
*/ | |
CalculatorRegistry.prototype.runCalculator = function(portfolio, name) { | |
return this.runCalculators(portfolio, this.getSingleCalculatorDependencyOrder(name)); | |
}; | |
/** | |
* Run all calculators in dependency order. Returns a promise, which will | |
* be resolved with a result set once all calculators are complete. The | |
* result set is an object keyed on calcualtor naeme, so | |
* `resultSet['some-calculator']` contains the results of the the calculator | |
* called 'some-calculator'. (There may be more than one if the calculator | |
* has dependencies.) | |
*/ | |
CalculatorRegistry.prototype.runAll = function(portfolio) { | |
return this.runCalculators(portfolio, this.getDependencyOrder()); | |
}; | |
// Helpers | |
CalculatorRegistry.prototype.getDependencyOrder = function() { | |
var sorter = new Toposort(); | |
_.each(this.dependencies, function(deps, name) { | |
sorter.add(name, deps); | |
}); | |
return sorter.sort().reverse(); | |
}; | |
CalculatorRegistry.prototype.getSingleCalculatorDependencyOrder = function(name) { | |
var self = this, | |
sorter = new Toposort(); | |
function addDependenciesOf(name) { | |
var deps = self.dependencies[name]; | |
if(deps) { | |
sorter.add(name, deps); | |
deps.forEach(function(dep) { | |
addDependenciesOf(dep); | |
}); | |
} | |
} | |
addDependenciesOf(name); | |
return sorter.sort().reverse(); | |
}; | |
CalculatorRegistry.prototype.runCalculators = function(portfolio, names) { | |
var self = this, | |
resultSet = {}, | |
promises = {}, | |
waitingFor = [], | |
deferred = promise.defer(); | |
names.forEach(function(name) { | |
var calculatorPromise = self.calculators[name](portfolio, promises); | |
promises[name] = calculatorPromise; | |
waitingFor.push(calculatorPromise); | |
calculatorPromise | |
.then( | |
function(result) { | |
resultSet[name] = result; | |
}, function(error) { | |
console.error("Error running calculator '" + name + "': " + error); | |
} | |
); | |
}); | |
promise.allOrNone(waitingFor).then(function() { | |
deferred.resolve(resultSet); | |
}, deferred.reject); | |
return deferred.promise; | |
}; | |
return CalculatorRegistry; | |
})(); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*jshint globalstrict:true, devel:true */ | |
/*global require, module, exports, process, __dirname, describe, before, after, it, expect */ | |
"use strict"; | |
var buster = require('buster'), | |
promise = require('node-promise'), | |
CalculatorRegistry = require('../../../calculators/registry'); | |
buster.spec.expose(); | |
describe("Calculator registry", function() { | |
it('does not fail with no calculators registered', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.runAll(portfolio) | |
.then(function(results) { | |
expect(results).toEqual({}); | |
done(); | |
}); | |
}); | |
it('can run a single calculator with no dependencies', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('test', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(42); | |
return deferred.promise; | |
}); | |
registry.runCalculator(portfolio, 'test') | |
.then(function(results) { | |
expect(results).toEqual({test: 42}); | |
done(); | |
}); | |
}); | |
it('can run a single calculator with no dependencies when running all calculators', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('test', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(42); | |
return deferred.promise; | |
}); | |
registry.runAll(portfolio) | |
.then(function(results) { | |
expect(results).toEqual({test: 42}); | |
done(); | |
}); | |
}); | |
it('only runs named calculators and their dependencies', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('test', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(42); | |
return deferred.promise; | |
}); | |
registry.add('norun', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(30); | |
return deferred.promise; | |
}); | |
registry.runCalculator(portfolio, 'test') | |
.then(function(results) { | |
expect(results).toEqual({test: 42}); | |
done(); | |
}); | |
}); | |
it('runs multiple calculators when calling runAll()', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('test', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(42); | |
return deferred.promise; | |
}); | |
registry.add('alsorun', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(30); | |
return deferred.promise; | |
}); | |
registry.runAll(portfolio) | |
.then(function(results) { | |
expect(results).toEqual({test: 42, alsorun: 30}); | |
done(); | |
}); | |
}); | |
it('runs dependencies', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('dep', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(30); | |
return deferred.promise; | |
}); | |
registry.add('notdep', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(20); | |
return deferred.promise; | |
}); | |
registry.add('test', ['dep'], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
promises['dep'].then(function(result) { | |
deferred.resolve(result + 12); | |
}); | |
return deferred.promise; | |
}); | |
registry.runCalculator(portfolio, 'test') | |
.then(function(results) { | |
expect(results).toEqual({test: 42, dep: 30}); | |
done(); | |
}); | |
}); | |
it('runs all items in dependency order', function(done) { | |
var registry = new CalculatorRegistry(), | |
portfolio = {}; // fake | |
registry.add('dep', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(30); | |
return deferred.promise; | |
}); | |
registry.add('notdep', [], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
deferred.resolve(20); | |
return deferred.promise; | |
}); | |
registry.add('test', ['dep'], function(portfolio, promises) { | |
var deferred = promise.defer(); | |
promises['dep'].then(function(result) { | |
deferred.resolve(result + 12); | |
}); | |
return deferred.promise; | |
}); | |
registry.runAll(portfolio) | |
.then(function(results) { | |
expect(results).toEqual({test: 42, dep: 30, notdep: 20}); | |
done(); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment