Skip to content

Instantly share code, notes, and snippets.

@optilude
Last active December 19, 2015 10:49
Show Gist options
  • Save optilude/5943423 to your computer and use it in GitHub Desktop.
Save optilude/5943423 to your computer and use it in GitHub Desktop.
Generic, asynchronous, dependency-aware, promise-based operations registry
/*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;
})();
/*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