Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin deps #662

Merged
merged 16 commits into from
Mar 13, 2013
280 changes: 280 additions & 0 deletions lib/ext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// Load modules

var Domain = require('domain');
var Boom = require('boom');
var Async = require('async');
var Utils = require('./utils');


// Declare internals

var internals = {};

/*
Extension functions use the following signature: function (request, next) { next(); }
*/

module.exports = internals.Ext = function () {

this._events = {
onRequest: null, // New request, before handing over to the router (allows changes to the request method, url, etc.)
onPreHandler: null, // After validation and body parsing, before route handler
onPostHandler: null // After route handler returns, before sending response
};
};


internals.Ext.prototype.add = function (event, func, options) {

return this._add(event, func, options);
};


internals.Ext.prototype._add = function (event, func, options, plugin) {

options = options || {};

Utils.assert(['onRequest', 'onPreHandler', 'onPostHandler'].indexOf(event) !== -1, 'Unknown event type: ' + event);

this._events[event] = this._events[event] || []

var ext = {
priority: this._events[event].length,
before: [].concat(options.before || []),
after: [].concat(options.after || []),
group: plugin || '?',
func: func
};

// Validate rules

Utils.assert(ext.before.indexOf(ext.group) === -1, 'Plugin ext cannot come before itself (' + ext.plugin + ')');
Utils.assert(ext.before.indexOf('?') === -1, 'Plugin ext cannot come before unassociated exts');
Utils.assert(ext.after.indexOf(ext.group) === -1, 'Plugin ext cannot come after itself (' + ext.plugin + ')');
Utils.assert(ext.after.indexOf('?') === -1, 'Plugin ext cannot come after unassociated exts');

// Insert event

this._events[event].push(ext);
this.sort(event);
};


internals.Ext.prototype.invoke = function (request, event, callback) {

var handlers = this._events[event]; // onRequest, onPreHandler, onPostHandler
if (!handlers) {
return callback();
}

Async.forEachSeries(handlers, function (ext, next) {

internals.Ext.runProtected(request.log.bind(request), event, next, function (run, protectedNext) {

run(function () {

ext.func(request, protectedNext);
});
});
},
function (err) {

return callback(err);
});
};


internals.Ext.runProtected = function (log, tags, callback, setup) {

var domain = Domain.createDomain();

// Ensure only one callback returned

var isFinished = false;
var finish = function () {

if (isFinished) {
log(['duplicate', 'callback', 'error'].concat(tags || []));
return;
}

isFinished = true;

domain.exit();
return callback.apply(null, arguments);
};

setup(function (run) {

domain.on('error', function (err) {

domain.dispose();
log(['uncaught'].concat(tags || []), err);
return finish(Boom.internal('Uncaught error', err));
});

// Execute functon

domain.enter();
run();
},
finish);
};


internals.Ext.prototype.sort = function (event) {

var exts = this._events[event];
if (!exts) {
return;
}

// Sort

var graph = internals.getGraph(exts);
var ancestors = internals.getAncestors(graph);
var sorted = internals.topoSort(ancestors, exts.length);

var priorityIndex = {};
exts.forEach(function (ext) {

priorityIndex[ext.priority] = ext;
});

this._events[event] = sorted.map(function (value) {

return priorityIndex[value];
});
};


internals.getGraph = function (exts) {

var groups = {};
var graph = {};
var graphAfters = {};

for (var i = 0, il = exts.length; i < il; ++i) {
var ext = exts[i];
var priority = ext.priority;
var group = ext.group;

// Determine Groups

if (groups.hasOwnProperty(group)) {
if (groups[group].indexOf(priority) == -1) {
groups[group].push(priority);
}
}
else {
groups[group] = [priority];
}

// Build intermediary graph using 'before'

var before = ext.before;
graph[priority] = (graph[priority] || []).concat(before);

// Build second intermediary graph with 'after'

var after = ext.after;
for (var j = 0, jl = after.length; j < jl; ++j) {
graphAfters[after[j]] = (graphAfters[after[j]] || []).concat(priority);
}
}

// Expand intermediary graph

Object.keys(graph).forEach(function (node) {

var expandedGroups = [];
for (var groupIndex in graph[node]) {
var group = graph[node][groupIndex];
groups[group] = groups[group] || [];
groups[group].forEach(function (d) {
expandedGroups.push(d);
});
}
graph[node] = expandedGroups;
});

// Merge intermediary graph using graphAfters into final graph

var afterNodes = Object.keys(graphAfters);
for (var n in afterNodes) {
var group = afterNodes[n];

for (var itemIndex in groups[group]) {
var node = groups[group][itemIndex];
graph[node] = (graph[node] || []).concat(graphAfters[group])
}
}

return graph;
};


internals.getAncestors = function (graph) {

var ancestors = {};
var graphNodes = Object.keys(graph);
for (var i in graphNodes) {
var node = graphNodes[i];
var children = graph[node];

for (var j = 0, jl = children.length; j < jl; ++j) {
ancestors[children[j]] = (ancestors[children[j]] || []).concat(node);
}
}

return ancestors;
};


internals.topoSort = function (ancestorsGraph, length) {

var visited = {};
var sorted = [];
length = length || ancestorsGraph.length;

var ancNodes = Object.keys(ancestorsGraph);

for (var i = 0, il = length; i < il; ++i) {
var next = i;

if (ancestorsGraph[i]) {
next = null;
for (var j = 0, jl = length; j < jl; ++j) {
if (visited[j] == true) {
continue;
}

if (!ancestorsGraph[j]) {
ancestorsGraph[j] = [];
}

var shouldSeeCount = ancestorsGraph[j].length;
var seenCount = 0;
for (var l = 0, ll = shouldSeeCount; l < ll; ++l) {
if (sorted.indexOf(ancestorsGraph[j][l]) >= 0) {
++seenCount;
}
}

if (seenCount == shouldSeeCount) {
next = j;
break;
}
}
}

if (next !== null) {
next = next.toString(); // Normalize to string
visited[next] = true;
sorted.push(next);
}
}

Utils.assert(sorted.length === length, 'Invalid ext dependencies detected');
return sorted;
};
16 changes: 10 additions & 6 deletions lib/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ internals.Pack.prototype.validate = function (plugin) {
return new Error('Plugin missing name');
}

if (plugin.name === 'hapi') {
return new Error('Plugin name cannot be \'hapi\'');
}

if (!plugin.version) {
return new Error('Plugin missing version');
}
Expand Down Expand Up @@ -202,16 +206,16 @@ internals.Pack.prototype._register = function (plugin, permissions, options, cal
}

if (permissions.state) {
methods.state = function (name, options) {
methods.state = function () {

self._applySync(selection.servers, Server.prototype.state, [name, options]);
self._applySync(selection.servers, Server.prototype.state, arguments);
};
}

if (permissions.helper) {
methods.helper = function (name, method, options) {
methods.helper = function () {

self._applySync(selection.servers, Server.prototype.helper, [name, method, options]);
self._applySync(selection.servers, Server.prototype.helper, arguments);
};
}

Expand All @@ -220,9 +224,9 @@ internals.Pack.prototype._register = function (plugin, permissions, options, cal
}

if (permissions.ext) {
methods.ext = function (event, func) {
methods.ext = function () {

self._applySync(selection.servers, Server.prototype.ext, [event, func]);
self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], plugin.name]);
};
}

Expand Down
Loading