hapi
Version:
HTTP Server framework
564 lines (390 loc) • 16.2 kB
JavaScript
// Load modules
var Path = require('path');
var Events = require('events');
var Async = require('async');
var Catbox = require('catbox');
var Server = require('./server');
var Views = require('./views');
var Utils = require('./utils');
var Defaults = require('./defaults');
// Declare internals
var internals = {};
internals.defaultPermissions = {
route: true,
helper: true,
state: true,
events: true,
views: true,
cache: true,
auth: true,
ext: false
};
exports = module.exports = internals.Pack = function (options) {
options = options || {};
Utils.assert(!options || !options.requirePath || options.requirePath[0] === '/', 'Pack option \'requirePath\' must be an absolute path');
this._settings = {
requirePath: options.requirePath || process.cwd() + '/node_modules'
};
this._servers = []; // List of all pack server members
this._byLabel = {}; // Server [ids] organized by labels
this._byId = {}; // Servers indexed by id
this._env = {}; // Plugin-specific environment (e.g. views manager)
this.list = {}; // Loaded plugins by name
this.events = new Events.EventEmitter(); // Consolidated subscription to all servers' events
this.app = options.app || {};
var cacheOptions = Catbox.defaults.apply(options.cache || Defaults.cache);
if (!options.cache ||
!options.cache.partition) {
cacheOptions.partition = 'hapi-cache';
}
this._cache = new Catbox.Client(cacheOptions); // Common cache (defaults to memory-based)
return this;
};
internals.Pack.prototype.server = function (arg1, arg2, arg3) {
[arg1, arg2, arg3].forEach(function (arg) { // Server arguments can appear in any order
if (typeof arg === 'object') {
Utils.assert(!arg.cache, 'Cannot configure server cache in a pack member');
}
});
this._server(new Server(arg1, arg2, arg3, this));
};
internals.Pack.prototype._server = function (server) {
var self = this;
var serverLabels = [].concat(server.settings.labels || []);
serverLabels = Utils.unique(serverLabels);
// Add server
var id = this._servers.length;
this._byId[id] = server;
this._servers.push(server);
// Add to labels
serverLabels.forEach(function (label) {
self._byLabel[label] = self._byLabel[label] || [];
self._byLabel[label].push(id);
});
// Subscribe to events
['log', 'request', 'response', 'tail', 'internalError'].forEach(function (event) {
server.on(event, function (request, data) {
self.events.emit(event, request, data);
});
});
};
internals.Pack.prototype.register = function (plugin/*, [options], callback */) {
// Validate arguments
var options = (arguments.length === 3 ? arguments[1] : null);
var callback = (arguments.length === 3 ? arguments[2] : arguments[1]);
var permissions = internals.defaultPermissions;
if (options instanceof Array) {
permissions = Utils.applyToDefaults(permissions, options[0] || {});
options = options[1] || null;
}
this._register(plugin, permissions, options, callback);
};
internals.Pack.prototype._register = function (plugin, permissions, options, callback, _dependencies) {
var self = this;
// Validate arguments
Utils.assert(plugin, 'Missing plugin');
Utils.assert(callback, 'Missing callback');
Utils.assert(!this._env[plugin.name], 'Plugin already registered:', plugin.name);
Utils.assert(plugin.name, 'Plugin missing name');
Utils.assert(plugin.name !== '?', 'Plugin name cannot be \'?\'');
Utils.assert(plugin.version, 'Plugin missing version');
Utils.assert(plugin.register && typeof plugin.register === 'function', 'Plugin missing register() method');
var dependencies = _dependencies || {};
// Setup environment
this._env[plugin.name] = {
name: plugin.name,
path: plugin.path
};
// Add plugin to servers lists
this.list[plugin.name] = plugin;
// Setup pack interface
var step = function (labels, subset) {
var selection = self._select(labels, subset);
var methods = {
length: selection.servers.length,
select: function (/* labels */) {
var labels = Utils.flatten(Array.prototype.slice.call(arguments));
return step(labels, selection.index);
},
api: function (/* key, value */) {
var key = (arguments.length === 2 ? arguments[0] : null);
var value = (arguments.length === 2 ? arguments[1] : arguments[0]);
selection.servers.forEach(function (server) {
server.plugins[plugin.name] = server.plugins[plugin.name] || {};
if (key) {
server.plugins[plugin.name][key] = value;
}
else {
Utils.merge(server.plugins[plugin.name], value);
}
});
}
};
if (permissions.route) {
methods.route = function (options) {
self._applySync(selection.servers, Server.prototype._route, [options, self._env[plugin.name]]);
};
}
if (permissions.state) {
methods.state = function () {
self._applySync(selection.servers, Server.prototype.state, arguments);
};
}
if (permissions.auth) {
methods.auth = function () {
self._applySync(selection.servers, Server.prototype.auth, arguments);
};
}
if (permissions.ext) {
methods.ext = function () {
self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], plugin.name]);
};
}
return methods;
};
// Setup root pack object
var root = step();
root.version = Utils.version();
root.hapi = require('./');
root.app = self.app;
root.path = plugin.path;
root.log = function (tags, data, timestamp) {
tags = (tags instanceof Array ? tags : [tags]);
var now = (timestamp ? (timestamp instanceof Date ? timestamp : new Date(timestamp)) : new Date());
var event = {
timestamp: now.getTime(),
tags: tags,
data: data
};
self.events.emit('log', event, Utils.mapToObject(event.tags));
};
root.dependency = function (deps) {
dependencies[plugin.name] = dependencies[plugin.name] || [];
deps = [].concat(deps);
deps.forEach(function (dep) {
if (!self._env[dep]) {
dependencies[plugin.name].push(dep);
}
});
};
if (permissions.events) {
root.events = self.events;
}
if (permissions.views) {
root.views = function (options) {
Utils.assert(!self._env[plugin.name].views, 'Cannot set plugin views manager more than once');
options.basePath = options.basePath || plugin.path;
self._env[plugin.name].views = new Views(options);
};
}
if (permissions.helper) {
root.helper = function () {
self._applySync(self._servers, Server.prototype.helper, arguments);
};
}
if (permissions.cache) {
root.cache = function (options) {
return self._provisionCache(options, 'plugin', plugin.name, options.segment);
};
}
// Register
plugin.register.call(null, root, options || {}, function (err) {
if (!_dependencies &&
dependencies[plugin.name]) {
Utils.assert(!dependencies[plugin.name].length, 'Plugin', plugin.name, 'missing dependencies:', dependencies[plugin.name].join(', '));
}
callback(err);
});
};
internals.Pack.prototype._select = function (labels, subset) {
var self = this;
Utils.assert(!labels || typeof labels === 'string' || labels instanceof Array, 'Bad labels object type (string or array required)');
var ids = [];
if (labels) {
[].concat(labels).forEach(function (label) {
ids = ids.concat(self._byLabel[label] || []);
});
ids = Utils.unique(ids);
}
else {
ids = Object.keys(this._byId);
}
var result = {
servers: [],
index: {}
};
ids.forEach(function (id) {
if (subset &&
!subset[id]) {
return;
}
var server = self._byId[id];
if (server) {
result.servers.push(server);
result.index[id] = true;
}
});
return result;
};
/*
name:
'plugin' - module in main process node_module directory
'./plugin' - relative path to file where require is called
'/plugin' - absolute path
{ 'plugin': { plugin-options } } - object where keys are loaded as module names (above) and values are plugin options
[ 'plugin' ] - array of plugin names, without plugin options
*/
internals.Pack.prototype.require = function (name/*, [options], callback*/) {
var options = (arguments.length === 3 ? arguments[1] : null);
var callback = (arguments.length === 3 ? arguments[2] : arguments[1]);
var permissions = internals.defaultPermissions;
if (options instanceof Array) {
permissions = Utils.applyToDefaults(permissions, options[0] || {});
options = options[1] || null;
}
this._require(name, permissions, options, callback);
};
internals.Pack.prototype._require = function (name, permissions, options, callback) {
var self = this;
Utils.assert(name && (typeof name === 'string' || typeof name === 'object'), 'Invalid plugin name(s) object: must be string, object, or array');
Utils.assert(!options || typeof name === 'string', 'Cannot provide options in a multi-plugin operation');
var callerPath = internals.getSourceFilePath(); // Must be called outside any other function to keep call stack size identical
var parse = function () {
var registrations = [];
if (typeof name === 'string') {
registrations.push({ name: name, options: options, permissions: permissions });
}
else if (name instanceof Array) {
name.forEach(function (item) {
registrations.push({ name: item, options: null, permissions: permissions });
});
}
else {
Object.keys(name).forEach(function (item) {
var reg = {
name: item,
options: name[item],
permissions: permissions
};
if (reg.options instanceof Array) {
reg.permissions = Utils.applyToDefaults(reg.permissions, reg.options[0] || {});
reg.options = reg.options[1] || null;
}
registrations.push(reg);
});
}
var dependencies = {};
Async.forEachSeries(registrations, function (item, next) {
load(item, dependencies, next);
},
function (err) {
Object.keys(dependencies).forEach(function (deps) {
dependencies[deps].forEach(function (dep) {
Utils.assert(self._env[dep], 'Plugin', deps, 'missing dependencies:', dep);
});
});
return callback(err);
});
};
var load = function (item, dependencies, next) {
var itemName = item.name;
if (itemName[0] === '.') {
itemName = Path.join(callerPath, itemName);
}
else if (itemName[0] !== '/') {
itemName = Path.join(self._settings.requirePath, itemName);
}
var plugin = null;
var mod = require(itemName); // Will throw if require fails
var pkg = require(Path.join(itemName, 'package.json'));
plugin = {
name: pkg.name,
version: pkg.version,
register: mod.register,
path: itemName
};
self._register(plugin, item.permissions, item.options, next, dependencies);
};
parse();
};
internals.getSourceFilePath = function () {
var stack = Utils.callStack();
var callerFile = '';
for (var i = 0, il = stack.length; i < il; ++i) {
var stackLine = stack[i];
if (stackLine[3].lastIndexOf('.require') === stackLine[3].length - 8) { // The file that calls require is next
callerFile = stack[i + 1][0];
break;
}
}
return Path.dirname(callerFile);
};
internals.Pack.prototype.allow = function (permissions) {
var self = this;
Utils.assert(permissions && typeof permissions === 'object', 'Invalid permission object');
var rights = Utils.applyToDefaults(internals.defaultPermissions, permissions);
var scoped = {
register: function (name, options, callback) {
var pluginPermissions = rights;
if (options instanceof Array) {
pluginPermissions = Utils.applyToDefaults(pluginPermissions, options[0] || {});
options = options[1] || null;
}
self._register(name, pluginPermissions, callback ? options : null, callback || options);
},
require: function (name, options, callback) {
var pluginPermissions = rights;
if (options instanceof Array) {
pluginPermissions = Utils.applyToDefaults(pluginPermissions, options[0] || {});
options = options[1] || null;
}
self._require(name, pluginPermissions, callback ? options : null, callback || options);
}
};
return scoped;
};
internals.Pack.prototype.start = function (callback) {
var self = this;
this._apply(this._servers, Server.prototype._start, null, function () {
self._cache.start(callback || function () { });
});
};
internals.Pack.prototype.stop = function (options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
this._apply(this._servers, Server.prototype._stop, [options], callback || function () { });
};
internals.Pack.prototype._apply = function (servers, func, args, callback) {
Async.forEachSeries(servers, function (server, next) {
func.apply(server, (args || []).concat([next]));
},
function (err) {
return callback(err);
});
};
internals.Pack.prototype._applySync = function (servers, func, args) {
for (var i = 0, il = servers.length; i < il; ++i) {
func.apply(servers[i], args);
}
};
internals.Pack.prototype._provisionCache = function (options, type, name, segment) {
Utils.assert(options, 'Invalid cache policy options');
Utils.assert(name, 'Invalid cache policy name');
Utils.assert(['helper', 'route', 'plugin'].indexOf(type) !== -1, 'Unknown cache policy type:', type);
if (type === 'helper') {
Utils.assert(!segment || segment.indexOf('##') === 0, 'Helper cache segment must start with \'##\'');
segment = segment || '#' + name;
}
else if (type === 'route') {
Utils.assert(!segment || segment.indexOf('//') === 0, 'Route cache segment must start with \'//\'');
segment = segment || name; // name (path) already includes '/'
}
else if (type === 'plugin') {
Utils.assert(!segment || segment.indexOf('!!') === 0, 'Plugin cache segment must start with \'!!\'');
segment = segment || '!' + name;
}
var policy = new Catbox.Policy(options, this._cache, segment);
return policy;
};