hunt
Version:
High level nodejs framework build on top of expressjs, mongoose, sequilize, socketio and passportjs
1,268 lines (1,183 loc) • 42.8 kB
JavaScript
'use strict';
var
async = require('async'),
winston = require('winston'),
appGenerator = require('./lib/http/expressApp.js'),
assert = require('assert'),
configGenerator = require('./lib/misc/config.js'),
crypt = require('./lib/misc/crypt.js'),
EventEmitter = require('eventemitter2').EventEmitter2,
fs = require('fs'),
glob = require('glob'),
mongooseGenerator = require('./lib/datastore/mongoClient.js'),
sequilizeGenerator = require('./lib/datastore/sqlClient.js'),
nodemailerListener = require('./lib/nodemailer.js'),
path = require('path'),
redisGenerator = require('./lib/datastore/redisClient.js'),
util = require('util');
winston.filters = winston.filters || [];
winston.rewriters = winston.rewriters || [];
/**
* @class Hunt
* @param {config} config - dictionary of configuration parameters
* @constructor
* @classdesc
* Constructor of HuntJS application
* @fires Hunt#start
* @fires Hunt#http:*
* @fires Hunt#http:success
* @fires Hunt#http:error
* @fires Hunt#user:*
* @fires Hunt#user:signup
* @fires Hunt#user:signin:*
* @fires Hunt#user:notify:*
* @fires Hunt#user:notify:all
* @fires Hunt#user:notify:email
* @fires Hunt#user:notify:sio
* @fires Hunt#user:save
* @fires Hunt#broadcast
* @fires Hunt#profiling:*
* @fires Hunt#profiling:redis:*
* @fires Hunt#profiling:mongoose:*
* @fires Hunt#profiling:sequilize:*
*
* @fires Hunt#REST:collectionName:CREATE:itemCreatedId
* @fires Hunt#REST:collectionName:READ:itemId
* @fires Hunt#REST:collectionName:UPDATE:itemId
* @fires Hunt#REST:collectionName:DELETE:itemId
* @fires Hunt#REST:collectionName:CALL_METHOD:methodName:itemId
* @fires Hunt#REST:collectionName:CALL_METHOD:methodName:itemId
*
*
* @example
* var hunt = require('hunt')({
* 'port': 3000
* })
* .extendRoutes(function (core) {
* core.app.get('/', function (req, res) {
* res.send('Hello, world!');
* });
* })
* .startWebServer();
*/
function Hunt(config) {
EventEmitter.call(this, {
wildcard: true,
delimiter: ':',
maxListeners: 20
});
//http://www.crockford.com/javascript/private.html
var prepared = false,
extendPassportStrategiesFunctions = [],
extendAppFunctions = [],
extendControllerFunctions = [],
extendTelnetFunctions = {};
/**
* @method Hunt#extendCore
* @param {string} field - property name to assign to hunt.[fieldName]
* @param {(string|object|Date|Array|function)} value - value to assign
* @description
* Inject public property to hunt core.
* If `value` is function, the `value` injected is result of value(hunt),
* else `value` injected is AS IS.
* @returns {Hunt} hunt object for functions chaining
* @example
* var Hunt = require('hunt')(configObj);
*
* //extending application core
*
* //assign value to field
* Hunt.extendCore('someVar', 42);
*
* //assign factory function to field and this field value
* //is result of this function
* Hunt.extendCore('someFuncToGetSomeVar', function (core) {
* return core.someVar;
* });
*
* //assign function to a core
* Hunt.extendCore('getSumWithSomeVar', function (core) {
* return function (a, b) {
* return a + b + core.someFuncToGetSomeVar;
* };
* });
*
* //Hunt core works after extending
* console.log('Sum of 2, 2 and 42 is ' + Hunt.getSumWithSomeVar(2, 2));
*/
this.extendCore = function (field, value) {
if (prepared) {
throw new Error('Hunt is prepared, unable to run hunt.extendCore');
}
if (typeof field === 'string' && value !== undefined) {
if (this[field] === undefined) {
if (typeof value === 'function') {
this[field] = value(this);
} else {
this[field] = value;
}
} else {
throw new Error('Unable to extend Hunt core. Field "' + field + '" already occupied!');
}
} else {
throw new Error('Unable to inject "' + field + '" with value "' + value + '" into hunt core! Type mismatch.');
}
return this;
};
/**
* @name Hunt#config
* @type {config}
* @description
* Object that represents current config object of HuntJS application
*/
this.extendCore('config', function () {
return configGenerator(config);
});
/**
* @method Hunt#exportModelToRest
* @param {ExportModelToRestParameters} parameters - for exporting model to REST interface properly
* @description
* Export models as REST api, exposed by current http server
*/
this.extendCore('exportModelToRest', require('./lib/exportModel/toRest.js'));
/**
* @name Hunt#async
* @type {Object}
* @description
* Embedded {@link https://www.npmjs.org/package/async | npm module of async} of 1.2.1 version for better workflow
*/
this.extendCore('async', function () {
return async;
});
/**
* @name Hunt#validator
* @type {Object}
* @description
* Embedded {@link https://www.npmjs.org/package/validator | npm module of validator} for better workflow
*/
this.extendCore('validator', function () {
return require('validator');
});
/**
* @name Hunt#Q
* @type {Object}
* @description
* Embedded {@link https://www.npmjs.org/package/q | npm module of Q} of 1.4.1 version for better workflow
*/
this.extendCore('Q', function () {
return require('q');
});
/**
* @name Hunt#http
* @type {Object}
* @description
* Embedded {@link http://nodejs.org/docs/latest/api/http.html | nodejs http module},
* that is used by socket.io and http server.
*/
this.http = require('http');
this.passport = require('passport');
this.rack = require('./lib/misc/rack.js');
/**
* @name Hunt#version
* @type {string}
* @description
* Current HuntJS version
*/
this.version = require('./package.json').version;
//placeholders, so we cannot break something by Hunt#extendCore
this.app = 'app';
this.httpServer = true;
this.sequelize = {};
this.mongoConnection = {};
this.mongoose = {};
this.io = {};
this.sessionStorage = true;
this.md5 = crypt.md5;
this.sha512 = crypt.sha512;
this.encrypt = function (text, secret) {
secret = secret || this.config.secret;
return crypt.encrypt(text, secret);
};
this.decrypt = function (text, secret) {
secret = secret || this.config.secret;
return crypt.decrypt(text, secret);
};
redisGenerator(this);
if (this.config.enableMongoose) {
mongooseGenerator(this);
}
if (this.config.enableSequilize){
sequilizeGenerator(this);
}
/**
* @name Hunt#model
* @type {model}
* @description
* Class to hold {@link model | data models} of Hunt application,
* they are injected into {@link request } object of controller.
* @see model
* @see request
*/
this.model = {};
/**
* @method Hunt#extendModel
* @param {string} modelName - property name to assign to hunt.model[modelName]
* @param {function} modelConstructor - factory function to generate model using Hunt kernel
* @see model
* @description
* Inject public property with model object to hunt.model.
* @returns {Hunt} hunt object
* @example
* //for mongoose model
* if (Hunt.config.enableMongoose) { //verify that we enabled mongoose
* Hunt.extendModel('Trophy', function (core) {
* var TrophySchema = new core.mongoose.Schema({
* 'name': {type: String, unique: true},
* 'scored': Boolean
* });
*
* TrophySchema.index({
* name: 1
* });
* //this step is very important - bind mongoose model to current mongo database connection
* // and assign it to collection in mongo database
* return core.mongoConnection.model('Trophy', TrophySchema);
* });
* // So, this model is accessible by
* // Hunt.model.Trophy
* };
*
* //sequilize model
* Hunt.extendModel('Planet', function (core) {
* var Planet = core.sequelize.define('Planet', {
* name: core.Sequelize.STRING //note that core.Sequilize != core.sequilize
* });
* return Planet;
* });
*/
this.extendModel = function (modelName, modelConstructor) {
if (prepared) {
throw new Error('Hunt is prepared, unable to run hunt.extendModel');
}
if (this.model[modelName] === undefined) {
if (typeof modelName === 'string' && typeof modelConstructor === 'function') {
this.model[modelName] = modelConstructor(this);
} else {
throw new Error('Unable to inject "' + modelName + '" into core! Wrong arguments!');
}
} else {
throw new Error('Unable to inject "' + modelName + '" into core! Model is already defined!');
}
return this;
};
/**
* @method Hunt#extendStrategy
* @tutorial webapplication
* @description
* Add new passportJS strategy to application by adding them to Hunt.passport object
* All strategies have to redirect to `/auth/success` on success or `/auth/failure`
* @param {object} Strategy object
* @see passport
* @example
*
* var HashStrategy = require('passport-hash').Strategy;
* //used for verify account by email
* exports.strategy = function (core) {
* return new HashStrategy(function (hash, done) {
* core.model.User.findOneByHuntKeyAndVerifyEmail(hash, function (err, userFound) {
* done(err, userFound);
* });
* });
* };
*
* exports.routes = function (core) {
* core.app.get('/auth/confirm/:hash', core.passport.authenticate('hash', {
* successRedirect: '/auth/success',
* failureRedirect: '/auth/failure'
* }));
* };
*
* @returns {Hunt} hunt object
*/
this.extendStrategy = function (Strategy) {
if (typeof Strategy.strategy === 'function' && typeof Strategy.routes === 'function') {
extendPassportStrategiesFunctions.push(Strategy);
} else {
throw new Error('Unable to extend passportjs strategies by Hunt.extendStrategy() with argument ' + JSON.stringify(Strategy) + '!');
}
return this;
};
/**
* @method Hunt#extendApp
* @tutorial webapplication
* @see Hunt#app
* @description
* Set expressJS application parameters - template engine, variables, locals
* {@link http://expressjs.com/api.html#app.engine | template engines},
* {@link http://expressjs.com/api.html#app.locals | locals} and
* {@link http://expressjs.com/api.html#app-settings | other}
* settings.
* In code it is called after setting logging middleware and port.
*
* @param {(string|Array|null)} environment - application environment to use,
* can be something like 'development', ['development','staging'] or null
*
* @param {function} settingsFunction - function(core){....}
* @example
*
* //example of setting template engine
* hunt.extendApp = function (core) {
* core.app.set('views', __dirname + '/views');
* core.app.set('view engine', 'ejs');
* core.app.set('layout', 'layout');
* core.app.enable('view cache');
* core.app.engine('ejs', require('ejs'));
* };
*
*
* @returns {Hunt} hunt object
*/
this.extendApp = function (environment, settingsFunction) {
if (prepared) {
throw new Error('Hunt application is already prepared! WE CAN\'T EXTEND IT NOW!');
}
var environmentToUse = null,
i,
j;
if (settingsFunction === undefined) {
settingsFunction = environment;
environment = null;
}
if (typeof environment === 'string') {
environmentToUse = [];
environmentToUse.push(environment);
}
if (environment instanceof Array) {
environmentToUse = environment;
for (i = 0; i < environment.length; i = i + 1) {
if (typeof environment[i] !== 'string') {
throw new Error('Hunt.extendApp requires environment name to be a string!');
}
}
}
if (typeof settingsFunction === 'function') {
if (environmentToUse) {
for (j = 0; j < environmentToUse.length; j = j + 1) {
extendAppFunctions.push({
'environment': environmentToUse[j],
'settingsFunction': settingsFunction
});
}
} else {
extendAppFunctions.push({
'settingsFunction': settingsFunction
});
}
} else {
throw new Error('Wrong arguments for extendApp(arrayOrStringOfEnvironments,settingsFunction)');
}
return this;
};
/**
* @method Hunt#extendController
* @param {string} mountPoint
* @param {function} settingsFunction
* @returns {Hunt} hunt object
* @see Hunt#startWebServer
* @see Hunt#express
* @see Hunt#extendApp
* @since 0.2.2
* @tutorial webapplication
* @description
* Add custom {@link http://expressjs.com/4x/api.html#router | router } for expressjs application
* @example
*
* hunt.extendController('/', function(core, router){
* router.get('/', function(request,response){
* response.render('index',{'title':'Hello!'});
* });
* });
* //this controller overwrites the first one!
* hunt.extendController('/', function(core, router){
* router.get('/', function(request,response){
* response.render('index2',{'title':'Hello!'});
* });
* });
*
*/
this.extendController = function (mountPoint, settingsFunction) {
if (prepared) {
throw new Error('Hunt application is already prepared! WE CAN\'T EXTEND IT NOW!');
}
if (typeof settingsFunction === 'function' && typeof mountPoint === 'string') {
extendControllerFunctions.push({
'mountPoint': mountPoint,
'settingsFunction': settingsFunction
});
} else {
throw new Error('Wrong argument for Hunt.extendController(mountPoint,function(core, router){...});');
}
return this;
};
/**
* @method Hunt#extendTelnet
* @param {string} command - command name
* @param {function} callback - function(core, client, payload){}
* @description
* Adds new behavior to telnet server. The callback has 2
* arguments, first one is core object, and second one is
* {@link https://github.com/andris9/rai | RAI client object }
* @example
*
*
* hunt.extendTelnet('version', function(core, client){
* client.send('HuntJS version is '+core.version);
* });
* hunt.extendTelnet('echo', function(core, client, payload){
* client.send(payload);
* });
*
*
* @since 0.0.18
* @tutorial telnetApplication
* @returns {Hunt} hunt object
*/
this.extendTelnet = function (command, callback) {
command = command.toLowerCase();
if (extendTelnetFunctions[command] === undefined) {
extendTelnetFunctions[command] = callback;
} else {
throw new Error('Telnet server behavior for command of "' + command + '" is already defined!');
}
return this;
};
/**
* @method Hunt#onSocketIoEvent
* @param {string} eventName
* @param {function} callback - function(payload, socket, function(error){...});
* @since 0.2.0
* @returns {Hunt} hunt object
* @example
* //client side code
* <script>
* var socket = io(); // TIP: io.connect() with no args does auto-discovery
* socket.emit('message', 'I will find you!', function (data) {
* console.log(data); // data will be 'cool!'
* });
* socket.send('I will find you!', function (data) { //the same
* console.log(data); // data will be 'cool!'
* });
* </script>
*
* //server side code
* hunt.onSocketIoEvent('message', function(payload, socket, fn){
* console.log('Message recieved',payload);
* socket.emit('thanks','for the message');
* fn('cool!');
* });
*/
this.onSocketIoEvent = function (eventName, callback) {
var h = this;
h.once('start', function (startParameters) {
//we process socket.io events here.
//note, that Hunt.io is generated only after
//application is started
if (startParameters.type === 'webserver' && h.config.io && h.config.io.enabled === true) {
//if application is started as background service, it do not have socket.io support
h.io.sockets.on('connection', function (socket) {
socket.on(eventName.toString(), function (payload, cb) {
callback(payload, socket, cb);
});
});
}
});
return this;
};
/**
* @class model
* @classdesc
* Class to hold data models of Hunt application, they are injected into
* request object. This models can be loaded by
* {@link Hunt#extendModel} function as mongoose or sequilize
* active record objects
* @property {User} Users - class, that represents site/application user
* @property {User} User - synonim for {@link User}
* @property {User} user - synonim for {@link User}
* @property {User} users - synonim for {@link User}
* @property {Message} Message - Class for messages send by users to groups
* @property {Message} messages - synonim for {@link Message}
* @property {Message} message - synonim for {@link Message}
* @see Hunt#extendModel
*/
(function (h) {
if (h.config.enableMongoose && h.config.enableMongooseUsers) {
var
mongooseUsers = require('./lib/models/user.mongoose.js'),
mongooseMessages = require('./lib/models/message.mongoose.js');
//user model
h.extendModel('User', mongooseUsers);
h.model.Users = h.model.User;
h.model.user = h.model.User;
h.model.users = h.model.User;
//sanity checks on model of User
[
'findOneByHuntKey', 'findOneByEmailOrUsername', 'findOneFuzzy', 'findById', 'find',
'signUp', 'signIn', 'findOneByHuntKeyAndVerifyEmail', 'findOneByHuntKeyAndResetPassword',
'processOAuthProfile'
]
.map(function (f) {
assert(typeof h.model.User[f] === 'function', 'Hunt.model.User.' + f + ' is not a function!');
});
//private messages model
h.extendModel('Message', mongooseMessages);
h.model.Messages = h.model.Message;
h.model.messages = h.model.Message;
h.model.message = h.model.Message;
}
nodemailerListener(h);
}(this));
function buildExpressApp(h) {
appGenerator.call(h, extendAppFunctions, extendControllerFunctions);
}
function buildTelnet(h) {
h.extendedCommands = extendTelnetFunctions;
}
/**
* @method Hunt#startBackGround
* @fires Hunt#start
* @description
* Starts Hunt application as single threaded background application
* It have redis client and data models, event emitting system exposed.
* It makes Hunt to emit event of "start" with payload of `{'type':'background'}`
* @example
*
* Hunt.startBackGround();
*
*/
this.startBackGround = function () {
winston.verbose('Trying to start Hunt as background service...');
buildExpressApp(this); //it is required for using templating system and `views` directory via `hunt.app.render()`
prepared = true;
/**
* Emitted when Hunt is started as background process
*
* @see Hunt#startBackGround
* @event Hunt#start
* @type {object}
* @property {string} type - with a value of string of 'background'
*/
this.emit('start', {'type': 'background'});
winston.info('Started Hunt as background service with PID#%d!', process.pid);
return this;
};
/**
* @method Hunt#startWebServer
* @fires Hunt#start
* @tutorial webapplication
* @description
* Starts Hunt application as single threaded web server
* It have redis client/models, expressJS application and event emitting system exposed.
* It makes Hunt to emit event of "start" with payload of `{'type':'webserver', 'port':80}`
* @param {(number|null)} port - what port to use, if null - use port value from config
* @param {(string|null)} address - what address to bind to. Default is '0.0.0.0' - all IPv4 addresses. The address is populated from environment address of HUNTJS_ADDR
* @example
*
* Hunt.startWebServer(80);
* Hunt.startWebServer(80, '0.0.0.0');
* Hunt.startWebServer(80, 'fe80::7218:8bff:fe86:542b');
*
*/
this.startWebServer = function (port, address) {
var
p = port || this.config.port,
h = this;
address = address || this.config.address || '0.0.0.0';
winston.verbose('Trying to start Hunt as web server on %s:%s...', address, p);
buildExpressApp(this);
this.httpServer.listen(p, address, function () {
/**
* Emitted when Hunt is started as webserver process
*
* @see Hunt#startWebServer
*
* @event Hunt#start
* @type {object}
* @property {string} type - with a value of string of 'webserver'
* @property {number} port - with a value of port this application listens to
* @property {string} address - with a value of address application is bound to
*/
h.emit('start', {'type': 'webserver', 'port': p, 'address': address});
winston.info('Started Hunt as web server on %s:%s with PID#%s', address, p, process.pid);
prepared = true;
});
return this;
};
/**
* @method Hunt#startTelnetServer
* @tutorial telnetApplication
* @param {(number|null)} port what port to use, if null - use port value from config
* @param {(string|null)} [address="0.0.0.0"] - address to listen on, if null - use address from config
* @fires Hunt#start
* @since 0.0.18
* @description
* Start Hunt as single process telnet server
* @example
*
* Hunt.startTelnetServer(3003);
*
*/
this.startTelnetServer = function (port, address) {
this.extendCore('telnetHandler', require('./lib/telnet/telnet.js'));
buildExpressApp(this); //only for `app.render`
buildTelnet(this);
var
thishunt = this,
p = port || this.config.port,
RAIServer = require('rai').RAIServer,
telnetServer = new RAIServer(this.config.telnetServer);
address = address || this.config.address || '0.0.0.0';
winston.verbose('Trying to start Hunt as telnet server on %s:%s...', address, p);
prepared = true;
telnetServer.on('connect', this.telnetHandler);
telnetServer.on('error', function (error) {
throw error;
});
telnetServer.listen(p, address, function (error) {
if (error) {
throw error;
}
/**
* Emitted when Hunt is started as telnet process
*
* @see Hunt#startTelnetServer
* @event Hunt#start
* @type {object}
* @property {string} type - with a value of string of 'telnet'
* @property {string} port - with a value of string of port number
* @property {string} address - with a value of address application is bound to
*/
thishunt.emit('start', {'type': 'telnet', 'port': p, 'address': address});
winston.info('Started Hunt as web server on %s:%s with PID#%s', address, p, process.pid);
});
return this;
};
/**
* @method Hunt#startWebCluster
* @tutorial webapplication
* @param {(number|null)} port what port to use, if null - use port value from config
* @param {(number|null)} maxProcesses maximal number of web server processes to spawn, default - 1 process per CPU core
* @description
* Starts hunt application as single threaded background application,
* that controls, by the means of {@link http://nodejs.org/docs/latest/api/cluster.html | nodejs embedded cluster},
* web server processes. By default it spawns 1 process per CPU core.
* It makes Hunt to emit event of "start" with payload of `{'type':'background'}`
* in master process, and events of "start" with payload of `{'type':'webserver', 'port':80}`
* in web server processes.
* @fires Hunt#start
* @returns {Boolean} true if this is master process, false if this is worker process.
* @example
*
* Hunt.startWebCluster(80, 10000);
*
*/
this.startWebCluster = function (port, maxProcesses) {
var
p = port || this.config.port,
m = maxProcesses || 'max';
return this.startCluster({'port': p, 'web': m, 'telnet': 0, 'background': 0});
};
/**
* @method Hunt#startTelnetCluster
* @param {(number|null)} port what port to use, if null - use port value from config
* @param {(number|null)} maxProcesses maximal number of web server processes to spawn, default - 1 process per CPU core
* @description
* Starts hunt application as single threaded background application,
* that controls, by the means of {@link http://nodejs.org/docs/latest/api/cluster.html | nodejs embedded cluster},
* telnet server processes. By default it spawns 1 process per CPU core.
* It makes Hunt to emit event of "start" with payload of `{'type':'background'}`
* in master process, and events of "start" with payload of `{'type':'telnet', 'port':25}`
* in each of telnet server processes.
* @fires Hunt#start
* @returns {Boolean} true if this is master process, false if this is worker process.
* @example
*
* Hunt.startTelnetCluster(25, 10000);
*
*/
this.startTelnetCluster = function (port, maxProcesses) {
var
p = port || this.config.port,
m = maxProcesses || 'max';
return this.startCluster({'port': p, 'web': 0, 'telnet': m, 'background': 0});
};
/**
* @method Hunt#startBackGroundCluster
* @fires Hunt#start
* @param {(Number|null)} maxProcesses maximal number of web server processes to spawn, default - 1 process per CPU core
* @description
* Starts Hunt application as single threaded background application, that controls, by the means of
* {@link http://nodejs.org/docs/latest/api/cluster.html | cluster}, other background applications.
* By default it spawns 1 process per CPU core.
* @returns {Boolean} true if this is master process, false if this is worker process.
* @example
*
*
* var numberOfProcessesToSpawn = 10;
* Hunt.startBackGroundCluster(numberOfProcessesToSpawn);
*
*
*/
this.startBackGroundCluster = function (maxProcesses) {
winston.verbose('Trying to start Hunt as background cluster service...');
return this.startCluster({'web': 0, 'telnet': 0, 'background': maxProcesses});
};
/**
* @method Hunt#startCluster
* @param {object} parameters - configuration parameters
* @fires Hunt#start
* @description
* Start Hunt as cluster. `Parameters` is object with 4 field -
* - `web`, `background`, `telnet`, 'port'.
* The values of `web`,`background`, `telnet` are number of child processes to spawn.
* The value of `port` is port number for web server processes to listen to.
* It is worth notice, that in this case telnet server listens on `Hunt.config.port+1` port!
* @returns {Boolean} true if this is master process, false if this is worker process.
* @example
*
* Hunt.startCluster({ 'web':1, 'background':1, 'telnet': 1 });
* Hunt.startCluster({ 'web':'max', 'port':80, 'address':'127.0.0.1' });
* Hunt.startCluster({ 'background':'max' });
* Hunt.startCluster({ 'telnet':max, 'port':25 });
* Hunt.startCluster({ 'web':'max', 'telnet':'max' }); //i strongly do not recommend doing this!
*
* @todo - make unit tests
*/
this.startCluster = function (parameters) {
var
h = this,
cluster = require('cluster'),
worker,
numCPUs = require('os').cpus().length,
maxWorkers = Math.min(this.config.maxWorkers, numCPUs),
runtimeConfig = {},
i = 0;
['web', 'background', 'telnet'].map(function (a) {
if (parameters[a] === 'max') {
runtimeConfig[a] = maxWorkers;
}
if (parseInt(parameters[a], 10)) {
runtimeConfig[a] = parameters[a];
}
if (!parameters[a]) {
runtimeConfig[a] = 0;
}
});
runtimeConfig.port = parseInt(parameters.port, 10) || config.port || 3000;
runtimeConfig.address = parameters.port || config.address;
if ((runtimeConfig.web + runtimeConfig.background + runtimeConfig.telnet) <= maxWorkers) {
if (cluster.isMaster) {
winston.silly('Cluster : We have %d CPU cores present. We can use %d of them.', numCPUs, maxWorkers);
winston.silly('Cluster : We need to spawn %d web server processes!', runtimeConfig.web);
winston.silly('Cluster : We need to spawn %d background processes!', runtimeConfig.background);
winston.silly('Cluster : We need to spawn %d telnet server processes!', runtimeConfig.telnet);
winston.silly('Cluster : Also we need to spawn 1 background processes to rule them all!');
winston.silly('Cluster : Starting spawning processes...');
winston.info('Cluster : Master PID#%d is online!', process.pid);
// Fork workers.
for (i = 0; i < runtimeConfig.web; i = i + 1) {
worker = cluster.fork();
worker.send('be_webserver');
winston.silly('Cluster : Spawning web server worker #%d with PID#%d...', i, worker.process.pid);
}
for (i = 0; i < runtimeConfig.background; i = i + 1) {
worker = cluster.fork();
worker.send('be_background');
winston.silly('Cluster : Spawning background worker #%d with PID#%d...', i, worker.process.pid);
}
for (i = 0; i < runtimeConfig.telnet; i = i + 1) {
worker = cluster.fork();
worker.send('be_telnet');
winston.silly('Cluster : Spawning telnet server worker #%d with PID#%d...', i, worker.process.pid);
}
cluster.on('online', function (worker) {
winston.info('Cluster : Worker PID#%d is online!', worker.process.pid);
});
cluster.on('exit', function (worker) {
var exitCode = worker.process.exitCode;
winston.warn('Cluster : Worker PID#%d died with exit code %d! ! Trying to spawn spare one...', worker.process.pid, exitCode);
cluster.fork().send('be_webserver'); //todo process case for telnet server!
});
this.startBackGround(); // the master process is ran as background application
return true;
}
process.on('message', function (msg) {
switch (msg) {
case 'be_background':
h.startBackGround(); // the child process is ran as background application
break;
case 'be_webserver':
h.startWebServer(runtimeConfig.port, runtimeConfig.address);
break;
case 'be_telnet':
if (runtimeConfig.web > 0) {
h.startTelnetServer((runtimeConfig.port + 1), runtimeConfig.address);
} else {
h.startTelnetServer(runtimeConfig.port, runtimeConfig.address);
}
break;
default:
throw new Error('Unknown command of `' + msg + '` from master process!');
}
});
return false;
}
throw new Error('This configuration requires more workers, than allowed by `config.maxWorkers`! ');
};
}
/**
* @method Hunt#on
* @param {string} eventName
* @param {function} listener
* @returns {Hunt}
* @see Hunt#once
* @description
* Adds a listener to the end of the listeners array for the specified event.
* Calls can be chained.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* The default delimiter is ":"
* @example
*
* hunt
* .on('start', function (params) {
* console.log('Hunt is started as '+params.type+' on port '+params.port);
* })
* .on('httpError', console.error);
*
*/
/**
* @method Hunt#many
* @param {string} eventName
* @param {Number} howManyTimesToFire
* @param {function} listener
* @returns {Hunt}
* @see Hunt#once
* @description
* Adds a listener to the end of the listeners array for the specified event
* that will execute n times for the event before being removed.
* The listener is invoked only the first n times the event is fired, after which it is removed.
* Calls can be chained.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* The default delimiter is ":"
* @example
*
* hunt
* .once('start', function (params) {
* console.log('Hunt is started as '+params.type+' on port '+params.port);
* })
* .many('httpError', 1, console.error); //1 error is enough :-)
*
*/
/**
* @method Hunt#once
* @param {string} eventName
* @param {function} listener
* @returns {Hunt}
* @description
* Adds a one time listener for the event. This listener is invoked only the next time the event is fired, after which it is removed.
* Calls can be chained.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* The default delimiter is ":"
* @see Hunt#on
*/
/**
* @method Hunt#removeListener
* @param {string} eventName
* @param {function} listener
* @returns {Hunt} Hunt
* @description
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* Remove a listener from the listener array for the specified event.
* Caution: changes array indices in the listener array behind the listener.
* Calls can be chained.
*/
/**
* @method Hunt#emit
* @param {string|Array<string>} eventName
* @param {object} payload
* @param {object} [additionalOptionalPayload]
* @param {object} [oneMoreAdditionalOptionalPayload]
* @param {object} [evenOneMoreAdditionalOptionalPayload]
* @return boolean
* @description
* Makes Hunt emit an event. Returns true if event had listeners, false otherwise.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* The default delimiter is ":"
*
* @example
*
* var hunt = require('./index.js')();
* hunt.extendCore('a', 'b');
* console.log('hunt.a=', hunt.a);
*
* hunt.on('ping', function (msg, msg1) {
* console.log('+++ping+++', this.event, msg, msg1, '+++ping+++');
*});
*
* hunt.on('ping:2', function (msg, msg1) {
* console.log('+++ping:2+++', this.event, msg, msg1, '+++ping:2+++');
*});
*
* hunt.on('ping:*', function (msg, msg1) {
* console.log('+++ping:*+++', this.event, msg, msg1, '+++ping:*+++');
*});
*
*
* hunt.once('start', function (type) {
* console.log('Starting to send events...');
* hunt.emit(['ping', '2'], 'pong', 'lll');
* });
* hunt.startBackGround();
*/
/**
* @method Hunt#onAny
* @param {object} payload
* @param {object} [additionalOptionalPayload]
* @param {object} [oneMoreAdditionalOptionalPayload]
* @param {object} [evenOneMoreAdditionalOptionalPayload]
* @return boolean
* @see Hunt#onAny
* @see Hunt#emit
* @see Hunt#once
* @see Hunt#on
* @description
* Adds a listener that will be fired when any event is emitted.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* @example
*
* function vortex(value1, value2) {
* console.log('All events trigger this. This is for you: ', value1, value2);
* }
* hunt.onAny(vortex);
*
* if (iHaveChangedMyMindToDoThis) {
* hunt.offAny(vortex);
* }
*/
/**
* @method Hunt#offAny
* @param {object} payload
* @param {object} [additionalOptionalPayload]
* @param {object} [oneMoreAdditionalOptionalPayload]
* @param {object} [evenOneMoreAdditionalOptionalPayload]
* @return boolean
* @description
* Removes the listener that will be fired when any event is emitted.
* Hunt object is a {@link http://nodejs.org/docs/latest/api/events.html | standard nodejs event emitter object }, so it supports all event emitter methods.
* Also Hunt object possess the functions of {@link https://www.npmjs.org/package/eventemitter2 | EventEmitter2}
* @see Hunt#onAny
* @see Hunt#emit
* @see Hunt#once
* @see Hunt#on
* @example
*
* function vortex(value1, value2) {
* console.log('All events trigger this. This is for you: ', value1, value2);
* }
* hunt.onAny(vortex);
*
* if (iHaveChangedMyMindToDoThis) {
* hunt.offAny(vortex);
* }
*/
util.inherits(Hunt, EventEmitter);
Hunt.prototype.huntEmit = function (eventName, payload) {
return this.emit(eventName, payload);
};
Hunt.prototype.huntOn = function (eventName, handler) {
return this.on(eventName, handler);
};
Hunt.prototype.huntOnce = function (eventName, handler) {
return this.once(eventName, handler);
};
/**
* @method Hunt#stop
* @description
* Stops the current application - close database connections, etc.
*/
Hunt.prototype.stop = function () {
if (this.redisClient) {
this.redisClient.quit();
}
if (this.mongoose && this.mongoose.connection) {
this.mongoose.connection.close();
this.mongoose.disconnect();
}
if (this.sequelize) {
//this.sequelize.close();
}
//setTimeout(function(){
winston.verbose('Hunt process %d is stoping...', process.pid);
// process.exit();
//}, 100)
};
/**
* @method Hunt#loadControllersFromDirectory
* @param {String} dirname - path to directoriy with controller files.
* @description
* Automatically load custom controllers from directory.
* @example
* ```javascript
* //file `controllers/something.controller.js
* "use strict";
* exports.mountpoint = '/';
* exports.handler = function (core, router) {
* router.get('/', function(req,res){
* res.send('Doing something...');
* });
* }
*
* //file index.js - main entry point
* hunt.loadControllersFromDirectory('./controllers');
* hunt.startWebServer();
* ```
*/
Hunt.prototype.loadControllersFromDirectory = function (dirname) {
var h = this;
/*jslint node: true, stupid: true */
fs.readdirSync(dirname).map(function (ctrlFileName) {
/*jslint node: true, stupid: false */
if (/^([a-zA-Z0-9_]+)\.controller\.js$/.test(ctrlFileName)) {
var
filepath = path.resolve('./' + path.join(dirname, ctrlFileName)),
ctrl = require(filepath);
winston.silly('Mounting controller `%s` on `%s`', ctrlFileName, ctrl.mountpoint);
h.extendController(ctrl.mountpoint, ctrl.handler);
}
});
return this;
};
/**
* @method Hunt#loadModelsFromDirectory
* @param {String} dirname - path to directoriy with model files.
* @see Hunt#extendModel
* @see ExportModelToRestParameters
* @description
* Automatically load custom models from directory.
* @example
* //file `models/Trophy.model.js
* 'use strict';
*
* exports.modelName = 'Trophy';
* exports.mountPoints = ['/api/v1/trophy', '/api/v1/trophies'];
* exports.exportModel = true;
* exports.ownerId = 'author';
*
* exports.init = function (core) {
* var TrophySchema = new core.mongoose.Schema({
* //other model initialization code
* })
* };
* //file index.js - main entry point
* hunt.loadModelsFromDirectory('./models');
* hunt.startWebServer();
*/
Hunt.prototype.loadModelsFromDirectory = function (dirname) {
var h = this;
/*jslint node: true, stupid: true */
fs.readdirSync(dirname).map(function (modelFileName) {
/*jslint node: true, stupid: false */
var
filepath = path.resolve('./' + path.join(dirname, modelFileName)),
model = require(filepath);
winston.silly('Loading model `%s` into `%s`', modelFileName, model.modelName);
h.extendModel(model.modelName, model.init);
if (model.exportModel === true) {
model.mountPoints.map(function (mp) {
winston.silly('Exporting model `%s` on `%s`', model.modelName, mp);
h.exportModelToRest({
'mountPoint': mp,
'modelName': model.modelName,
'ownerId': model.ownerId
});
});
}
});
return this;
};
/**
* @method Hunt#injectCssFromDirectory
* @param {String} arrayOfPatterns - array of patterns for css files to load.
* @description
* Automatically load css files from directory and load in `hunt.app.locals.css`
* @example
*
* Hunt.injectCssFromDirectory(["public/custom/*.css","public/vendor/*.css"])
*/
Hunt.prototype.injectCssFromDirectory = function (arrayOfPatterns) {
var
t = this,
dicOfCss = {};
arrayOfPatterns.map(function (p) {
glob.sync(p).map(function (f) {
dicOfCss[f] = true;
});
});
t.once('start', function (payload) {
if (payload.type === 'webserver') {
t.app.locals.css = Object.keys(dicOfCss).map(function (k) {
k = '/'+path.relative(t.config.public, k);
winston.silly('Using CSS from %s',k);
return k;
});
}
});
return this;
};
/**
* @method Hunt#injectJsFromDirectory
* @param {String} arrayOfPatterns - array of patterns for css files to load.
* @description
* Automatically load client side javascript files from directory and load in `hunt.app.locals.javascripts`
* @example
*
* Hunt.injectJsFromDirectory(["public/custom/*.js","public/vendor/*.js"])
*/
Hunt.prototype.injectJsFromDirectory = function (arrayOfPatterns) {
var
dicOfJs = {},
t = this;
arrayOfPatterns.map(function (p) {
glob.sync(p).map(function (f) {
dicOfJs[f] = true;
});
});
t.once('start', function (payload) {
if (payload.type === 'webserver') {
t.app.locals.javascripts = Object.keys(dicOfJs).map(function(k){
k = path.relative(t.config.public, k);
winston.silly('Using client side js from %s',k);
return '/'+k;
});
}
});
return t;
};
process.on('uncaughtException', function (error) {
winston.error(error);
process.exit(1);
});
/**
* HuntJS framework module
* @module Hunt
* @constructs Hunt
* @param {config} config - key-value object with settings
*/
module.exports = exports = function (config) {
return new Hunt(config);
};