lazo-next
Version:
A client-server web framework built on Node.js that allows front-end developers to easily create a 100% SEO compliant, component MVC structured web application with an optimized first page load.
478 lines (400 loc) • 15.6 kB
JavaScript
define([
'handlers/app/main',
'handlers/assets',
'continuation-local-storage',
'cluster',
'fs',
'hapi',
'path',
'resolver/main',
'handlers/safe',
'handlers/stream',
'handlers/tunnel',
'lodash',
'forbidden',
'appServerSetup'
], function (appHandler, assetsHandler, cls, cluster, fs, hapi, path, resolver, safeHandler, streamHandler, tunnelHandler, _, forbidden, AppServerSetup) {
'use strict';
var lazoNs = cls.createNamespace('lazojs');
var appServerSetup = new AppServerSetup();
return {
initialize: function (errHandlers) {
LAZO.logger.info(['server.initialize'], 'Initializing server...');
this.errHandlers = errHandlers;
this._cluster(); // cluster server if specified else execute this._worker once
},
_servers: {},
_worker: function () {
var self = this;
this._create(function () {
self._extensions() // server extensions
._routes() // define app routes and add app handlers
._handlers() // add all other route handlers
._error() // init logger & error handling
._start();
});
},
_create: function (callback) {
var servers = LAZO.conf.server.instances;
var serverDefaults = LAZO.conf.server.defaults;
var self = this;
var options;
var port;
function getSslCerts(tls) {
for (var k in tls) {
try {
tls[k] = fs.readFileSync(path.resolve(LAZO.FILE_REPO_DIR + '/' + tls[k]));
} catch (e) {
throw new Error('could not find ssl ' + k + ' at ' + tls[k]);
}
}
}
this.pack = new hapi.Pack();
LAZO._servers = {};
for (var k in servers) {
options = _.merge({ labels: [k] }, servers[k], serverDefaults);
port = options.port;
options = _.omit(options, ['port', 'maxBytes']);
options.views = {
engines: {
hbs: {
module: require('handlebars')
}
},
allowAbsolutePaths: true
};
options.maxSockets = LAZO.conf.server.maxSockets;
if (options.tls) {
getSslCerts(options.tls);
}
LAZO._servers[k] = this._servers[k] = this.pack.server(port, options);
LAZO.logger.info(['server._create'], 'Lazo created server on port %d', port);
}
appServerSetup.setup(hapi, this.pack, this._servers, {
success: function () {
callback();
},
error: function (err) {
LAZO.logger.warn(['server.setup'], 'Error: ', err);
callback();
}
});
},
_extensions: function () {
var self = this;
function preResponseHandler (request, reply) {
var response = request.response;
if (forbidden(request.url.path)) {
return reply(hapi.error.forbidden());
} else if (response.isBoom) {
if (LAZO.CLUSTER && !LAZO.ROBUST && response.output.statusCode >= 500 && response.output.statusCode <= 599) {
LAZO.logger.warn('[server._extensions]', 'Internal server error', request.url.href, response.response.code, response.message, response.trace);
self._stop();
}
var errHandler = self.errHandlers[response.output.statusCode];
if (errHandler) {
return reply.view(errHandler.server, response.output.payload)
.code(response.output.statusCode);
}
}
return reply();
}
function onRequest(request, next) {
lazoNs.run(function () {
lazoNs.set('request', request);
process.nextTick(function () {
next();
});
});
}
for (var k in this._servers) {
this._servers[k].ext('onPreResponse', preResponseHandler);
this._servers[k].ext('onRequest', onRequest);
this._servers[k].ext('onPreHandler', onRequest);
}
return this;
},
_routes: function () {
var self = this;
var routeDefs = LAZO.routes;
var serverRoutDefs;
var s;
var r;
// TODO: do we still need to handle POST request for routes?
function processRoute(route, tags) {
var routes = resolver.transformRoute(route);
if (routes.routeNoTrailingSlash) {
self._addAppRouteHandler('GET', routes.routeNoTrailingSlash, route, tags);
self._addAppRouteHandler('POST', routes.routeNoTrailingSlash, route, tags);
}
self._addAppRouteHandler('GET', routes.route, route, tags);
self._addAppRouteHandler('POST', routes.route, route, tags);
}
// add default server tag if it does not exist
function setTags(routeDefs) {
for (var k in routeDefs) {
if (_.isObject(routeDefs[k])) {
if (!_.isArray(routeDefs[k].servers)) {
routeDefs[k].servers = ['primary'];
}
} else {
routeDefs[k] = {
component: routeDefs[k],
servers: ['primary']
};
}
}
return routeDefs;
}
serverRoutDefs = setTags(routeDefs);
for (r in serverRoutDefs) {
processRoute(r, serverRoutDefs[r].servers);
}
for (s in this._servers) {
// give server a reference to routes, used later when handling the routes
this._servers[s]._routes = serverRoutDefs;
}
return this;
},
_addAppRouteHandler: function (method, path, route, tags) {
var self = this,
definition = { method: method, path: path };
if (method === 'GET') {
definition.config = {
tags: tags,
handler: function (request, reply) {
appHandler(request, reply, route);
}
};
} else {
definition.config = {
tags: tags,
handler: function (request, reply) {
appHandler(request, reply, route);
},
payload: { parse: true }
};
}
this._setServerRoute(definition);
return this;
},
_handlers: function () {
this._tunnelHandler()
._assetsHandler()
._staticFilesHandler();
return this;
},
_tunnelHandler: function () {
var self = this;
var tunnelOptions = {
method: 'POST',
path: '/tunnel/{extras*}',
config: {
handler: tunnelHandler,
payload: { parse: true }
}
};
this._setServerRoute(tunnelOptions);
var postFcaFileOptions = {
method: 'POST',
path: '/fn/{compName}/upload',
config: {
handler: streamHandler,
payload: {
parse: true,
output: 'file'
}
}
};
this._setServerRoute(postFcaFileOptions);
var postFaFileOptions = {
method: 'POST',
path: '/fn/upload',
config: {
handler: streamHandler,
payload: {
parse: true,
output: 'file'
}
}
};
this._setServerRoute(postFaFileOptions);
var postFcaOptions = {
method: 'POST',
path: '/fn/{compName}/{action}',
config: {
handler: streamHandler,
payload: { parse: true }
}
};
this._setServerRoute(postFcaOptions);
var postFaOptions = {
method: 'POST',
path: '/fn/{action}',
config: {
handler: streamHandler,
payload: { parse: true }
}
};
this._setServerRoute(postFaOptions);
var getFcaOptions = {
method: 'GET',
path: '/fn/{compName}/{action}',
config: {
handler: streamHandler
}
};
this._setServerRoute(getFcaOptions);
var getFaOptions = {
method: 'GET',
path: '/fn/{action}',
config: {
handler: streamHandler
}
};
this._setServerRoute(getFaOptions);
return this;
},
_assetsHandler: function () {
var assetOptions = {
method: 'GET',
path: '/assets',
handler: assetsHandler
};
this._setServerRoute(assetOptions);
return this;
},
_staticFilesHandler: function () {
var pathsOptions = {
method: 'GET',
path: '/{path*}',
handler: {
directory: {
path: [
LAZO.FILE_REPO_DIR,
LAZO.BASE_PATH,
LAZO.BASE_PATH + '/lib'
],
listing: false,
index: true
}
}
};
this._setServerRoute(pathsOptions);
var libOptions = {
method: 'GET',
path: '/lib/{path*}',
handler: {
directory: { path: [LAZO.BASE_PATH + '/lib'], listing: true, index: true }
}
};
this._setServerRoute(libOptions);
var baseOptions = {
method: 'GET',
path: '/base/{path*}',
handler: {
directory: { path: [LAZO.BASE_PATH + '/base'], listing: true, index: true }
}
};
this._setServerRoute(baseOptions);
return this;
},
_error: function () {
var ev;
for (var k in this._servers) {
ev = this._servers[k].on ? this._servers[k] : this._servers[k].events;
ev.on('internalError', function (request, error) {
LAZO.logger.debug('[server._error]', 'Internal server error', request, error);
});
}
return this;
},
_start: function () {
var self = this;
this.pack.register({
plugin: require('crumb'),
options: { ext: true }
}, function (err) {
if (!err) {
self.pack.start(function () {
appServerSetup.afterInitialize(hapi, self.pack, self.servers, {
success: function () {
LAZO.logger.info(['server.afterInitialize'], 'Server initialized...');
},
error: function (err) {
LAZO.logger.warn('[server.afterInitialize]', 'Error while Initializing...', err);
}
});
});
} else {
LAZO.logger.error('[server._start]', 'Error while starting %s server...', k, err);
}
});
return this;
},
_stop: function () {
var self = this;
try {
if (cluster.isWorker) {
cluster.worker.disconnect();
}
this.pack.stop(function () {
process.exit(1);
});
} catch (err) {
LAZO.logger.error('[server._stop]', 'Error while stopping server and disconnecting worker.', err);
}
},
_cluster: function () {
var cpus;
var k;
var port;
if (!LAZO.CLUSTER) {
this._worker();
for (k in this._servers) {
port = LAZO.conf.server.instances[k].port || LAZO.conf.server.defaults.port;
LAZO.logger.warn('[server._cluster]', 'Server %s started with single process, listening on port %d...', port, k);
}
return this;
}
cpus = process.env['CLUSTER'];
if (cluster.isMaster) {
for (var i = 0; i < cpus; i++) {
cluster.fork();
}
cluster.on('online', function (worker) {
LAZO.logger.warn('[server._cluster]', 'Worker %d:%d is online.', worker.id, worker.process.pid);
});
cluster.on('disconnect', function (worker) {
LAZO.logger.warn('[server._cluster]', 'Worker %d:%d was disconnected.', worker.id,
worker.process.pid);
cluster.fork();
});
} else {
this._worker();
for (k in this._servers) {
port = LAZO.conf.server.instances[k].port || LAZO.conf.server.defaults.port;
LAZO.logger.warn('[server._cluster]', 'Server %s started with %d workers, listening on port %d...', k, cpus, port);
}
}
return this;
},
_setServerRoute: function (options) {
var self = this;
for (var k in this._servers) {
// check for server name in application route tags
// if server name is not in tags then do not add route
if (options.config && options.config.tags &&
!_.find(options.config.tags, function (tag) {
return tag === k;
})) {
continue;
}
if (LAZO.ROBUST && typeof options.handler === 'function') {
options.handler = safeHandler(this._servers[k], options.handler, self);
}
this._servers[k].route(options);
}
}
};
});