scalra
Version:
node.js framework to prototype and scale rapidly
650 lines (518 loc) • 19.5 kB
JavaScript
//
// handler.js
//
// Storage of all event handlers and how they're been processed
//
// history:
// 2014-03-04 extracted from event_manager.js
//
// functions:
//
// add add some handlers to a set under a given name
// addByFile add some handlers to a set under a given name
// get get a particular event handler set by name
// dispatch dispatch an event to be processed by a specific set of handlers
//
var l_name = 'SR.Handler';
var l_handlerSets = {};
// add some handlers to a set under a given name
var l_add = exports.add = function (handlers, name) {
// set default name if not exist
name = name || 'default';
return l_get(name).load(handlers);
};
// add some handlers by filename and owner
// handler_info example:
// {name: 'handler', file: 'handler.js'},
// {name: 'system', file: 'system', owner: 'SR'},
// {name: 'cluster', file: 'cluster.js', owner: 'SR'}
// from listener.js
// TODO: merge handler loading in SR.Script?
// TODO: cleaner way to do parameter passing and owner lookup
// NOTE: if owner or path is specified, then the handler will be reloadable by default
var l_addByFile = exports.addByFile = function (handler_info, path) {
var filename = handler_info.file;
var handler_name = handler_info.name || filename;
var owner = handler_info.owner;
// define full path to the handler file
var fullpath = undefined;
// remove ending .js
if (handler_name.endsWith('.js') || handler_name.endsWith('.JS')) {
handler_name = handler_name.slice(0, handler_name.length-3).replace('/', '_');
}
if (filename) {
// attach '.js' if not exist
if (filename.endsWith('.js') === false && filename.endsWith('.JS') === false)
filename += '.js';
// if owner is specified
if (owner) {
if (owner === 'SR' || owner === 'scalra') {
fullpath = SR.path.join(__dirname, '..', 'handlers', filename);
} else if (SR.Settings.PATH_LIB) {
fullpath = SR.path.join(SR.Settings.PATH_LIB, owner, filename);
}
}
// otherwise we assume in same directory
else if (path)
fullpath = SR.path.join(path, filename);
}
// set up script monitor, so we may hot-load handler functions
if (SR.Script.monitor(handler_name, fullpath) === undefined) {
LOG.error('cannot load file: ' + fullpath, l_name);
return false;
}
LOG.warn('load handlers [' + handler_name + '] success...', l_name);
var handlers = SR.Script[handler_name];
// add handlers by checkers & handlers array
l_add(handlers);
return true;
};
// get a particular event handler set by name
var l_get = exports.get = function (name) {
// set default name if not exist
name = name || 'default';
// create new set if not exists
if (l_handlerSets.hasOwnProperty(name) === false) {
l_handlerSets[name] = new EventHandler();
LOG.sys('creating new handler_set: ' + name, l_name);
}
return l_handlerSets[name];
};
// dispatch an event to be processed by a specific set of handlers
exports.dispatch = function (event, name) {
// set default name if not exist
name = name || 'default';
// find handler set
if (l_handlerSets.hasOwnProperty(name) === false) {
LOG.warn('no handler set by the name [' + name + ']', l_name);
return false;
}
return l_handlerSets[name].dispatcher(event);
};
// set permission to use a particular event handler
exports.setGroup = function (arg/*event_name, group, type, name*/) {
// set default name if not exist
arg.name = arg.name || 'default';
return l_get(name).setGroup(arg);
};
//todo:
exports.getGroup = function (arg) {
};
//
// a EventHandler object, for handling incoming packets,
// given customized handlers for different event types
//
// functions:
// getHandlerSize()
// getHandlers()
// load(handlers)
// dispatcher(event)
// addResponder(response_type, callback)
// setGroup({event_name:event_name, group:group, type:type})
var EventHandler = function () {
// format checker
var l_checkers = {};
// event handler
var l_handlers = {};
// register response callback (event responder) for server notifications
var l_responders = {};
// get checkers for external verification / doc generation
this.getCheckers = function () {
return l_checkers;
};
// return the number of handlers
this.getHandlerSize = function () {
return Object.keys(l_handlers).length;
};
// return all current handlers
this.getHandlers = function () {
return l_handlers;
};
// add custom handlers to this EventHandler
this.load = function (handler_app) {
if (typeof handler_app !== 'object') {
LOG.error('nothing to load', l_name);
LOG.stack();
return 0;
}
// get checker & handler
var checkers = handler_app.checkers;
var handlers = handler_app.handlers;
// do check
if (typeof handler_app.getFormatCheckers === 'function')
checkers = handler_app.getFormatCheckers();
if (typeof handler_app.getMessageHandlers === 'function')
handlers = handler_app.getMessageHandlers();
if (!checkers || !handlers) {
LOG.debug('Checkers or handlers not found, possibly only SR.Callback used?', l_name);
return 0;
}
// store checkers & handlers locally
var num_stored = 0;
var list = '';
for (var h in handlers) {
// check if format checker is available
if (checkers.hasOwnProperty(h))
l_checkers[h] = checkers[h];
// store event handler
l_handlers[h] = handlers[h];
list += (h + ' ');
num_stored++;
}
LOG.sys(list, l_name);
return num_stored;
};
// TODO: suitable to put here?
// add custom handlers to this EventHandler
// NOTE: return current permission settings
this.setGroup = function (arg /*event_name, group, type*/) {
// check if name exists
if (l_checkers.hasOwnProperty(arg.event_name) === false) {
LOG.warn('event handler [' + arg.event_name + '] does not exist, cannot modify permission', l_name);
return undefined;
}
LOG.warn('type: ' + arg.type, l_name);
// check over if groups exist
var checkers = l_checkers[arg.event_name];
var groups = (checkers.hasOwnProperty('_groups') ? l_checkers[arg.event_name]['_groups'] : []);
var permissions = (checkers.hasOwnProperty('_permissions') ? l_checkers[arg.event_name]['_permissions'] : []);
LOG.sys('original groups & permissions', l_name);
LOG.sys(groups, l_name);
LOG.sys(permissions, l_name);
for (var i=0; i < groups.length; i++) {
if (groups[i] === arg.group) {
if (arg.type === true) {
LOG.warn('group [' + arg.group + '] already set for event [' + arg.event_name + ']', l_name);
return groups;
}
// unset flag
else
break;
}
}
for (var i=0; i < permissions.length; i++) {
if (permissions[i] === arg.permission) {
if (type === true) {
LOG.warn('permission [' + arg.permission + '] already set for event [' + arg.event_name + ']', l_name);
return permissions;
}
// unset flag
else
break;
}
}
// if not found, check if this is a new group to add
if (i === groups.length) {
if (type === true) {
LOG.warn('adding new group [' + group + '] to event [' + event_name + ']', l_name);
groups.push(group);
} else {
LOG.warn('no group setting', l_name);
return groups;
}
} else
groups.splice(i, 1);
// re-assign
if (groups.length > 0)
l_checkers[event_name]['_groups'] = groups;
else
delete l_checkers[event_name]['_groups'];
LOG.warn('new groups: ', l_name);
LOG.warn(groups, l_name);
return groups;
};
// check if an event should be forwarded to another app server for execution
var l_checkForward = function (msgtype, event) {
// we only forward for non-SR user-defined events at lobby
if (SR.Settings.SERVER_INFO.type !== 'lobby' || msgtype.startsWith('SR'))
return false;
// check if we're lobby and same-name app servers are available
var list = SR.AppConn.queryAppServers();
LOG.sys('check forward for: ' + msgtype + ' app server size: ' + Object.keys(list).length, l_name);
var minload_id = undefined;
var minload = 10000;
for (var id in list) {
var info = list[id];
if (info.type === 'app' && info.name === SR.Settings.SERVER_INFO.name) {
LOG.warn('found forward target [' + id + '] loading: ' + info.usercount, l_name);
if (info.usercount < minload) {
minload_id = id;
minload = info.usercount;
}
}
}
// an app server with minimal loading is available, relay the event
if (minload_id) {
SR.RPC.relayEvent(minload_id, msgtype, event);
return true;
}
// no need to forward, local execution
return false;
};
//
// Dispatcher for sending a particular command to its handler
//
//-----------------------------------------
// NOTE: dispatcher requires the following data:
// l_responders (responders for events sent to server)
// l_handlers (handlers for events)
// l_checkers (format checkers for events)
// TODO: remove all eventtype check (leave only one)
// NOTE: 'err' should not be received
var eventtypes = [SR.Tags.EVENT, SR.Tags.UPDATE];
this.dispatcher = function (event) {
// extract message type
for (var i=0; i < eventtypes.length; i++) {
if (event.data[eventtypes[i]])
break;
}
// unknown event type
if (i == eventtypes.length) {
var err_str = 'unknown event type. sent from: ' + event.printSource();
LOG.error(err_str, l_name);
// print each key in this event
for (var k in event.data)
LOG.error(k, l_name);
// simply ignore, this should not happen
SR.EventManager.checkout(event, {});
return false;
}
// narrow down event type
var eventtype = eventtypes[i];
var msgtype = event.data[eventtype];
event.msgtype = msgtype;
// lookup name associated with this connection
var conn_name = '';
// will look up for connection name & pass in
if (event.conn !== undefined)
conn_name = SR.Conn.getSessionName(event.conn);
// log incoming message type
// append '\n' at end to indicate message end
// NOTE: message type is shown as 'debug' message to allow developer also to see it
var recv_str = JSON.stringify(event.data) + '\n';
LOG.debug(SR.Tags.RCV + msgtype + ' from ' + (conn_name ? '[' + conn_name + '] ' : '') + '(' + event.printSource() + ')\n' + recv_str + SR.Tags.END, l_name);
// record incoming size plus '\n'
SR.Stat.add('net_in', recv_str.length + 1);
// transfer cid & parameters up one level
if (event.data['_cid'])
event.cid = event.data._cid;
//LOG.warn('before event obj:', l_name);
//LOG.warn(event);
// NOTE: somehow the hasOwnProperty check will pass if event.data does not have the SR.Tags.PARA field
if (!event.data[SR.Tags.PARA])
event.data = {};
else
event.data = event.data[SR.Tags.PARA];
//LOG.warn('after event obj:', l_name);
//LOG.warn(event);
// move rid (request id) into event object, if exists
// NOTE: do not store in conn object, as different events could shae the SAME connection object
// for socket connections
if (event.data['_rid']) {
event.rid = event.data._rid;
delete event.data['_rid'];
}
// check if this message is a response from a previous query (often to another frontier)
// if so, a unique client ID will be attached
// NOTE: we assume the msgtype stored in l_responders
// will not duplicate with any msgtype in l_handlers
if (l_responders.hasOwnProperty(msgtype) === true) {
// handle directly from callback pool
if (event.hasOwnProperty('cid'))
l_handleEventResponse(msgtype, event);
else
LOG.error('no client id (cid) provided by a response for event [' + msgtype + ']', l_name);
// finish handling the event
// NOTE: should not use 'event.done' as it will return something back, which may trigger new responses
SR.EventManager.checkout(event, {});
return;
}
// check if the right handler exists (for both format & content)
if (l_handlers.hasOwnProperty(msgtype) === false) {
var err_str = 'no handler for [' + eventtype + '] type: ' + msgtype;
LOG.error(err_str, l_name);
LOG.error('existing handlers: ', l_name);
for (var event_name in l_handlers)
LOG.error(event_name, l_name);
// notify error
var obj = {};
obj[SR.Tags['UPDATE']] = SR.Tags['RES_ERROR'];
obj[SR.Tags['PARA']] = {msg: err_str};
SR.EventManager.checkout(event, obj);
return;
}
// check if parameter format is correct
// NOTE: this check will allow unspecified SR.Tags.PARA to pass through
// will also allow other types of parameters be passed through
// assumpion here is that the handler will not process non-SR.Tags.PARA parameters
//var result = true;
var err_str = [];
// if checker exist, perform format check
if (l_checkers.hasOwnProperty(msgtype) === true) {
var checker = l_checkers[msgtype];
if (typeof checker === 'function') {
if (UTIL.safeCall(checker, event.data, event.session) === false) {
err_str.push('checker function fail');
}
} else {
// it's a js object, check each parameter one by one
for (var para in checker) {
if (para.charAt(0) === '_') {
// check for login requirement (must first login before proceed)
if (para === '_login') {
// NOTE: this check will be passed automatically if caller the server
// that is, as internal API this check is bypassed automatically
// because 'event.session' likely does not exist
if (checker[para] === true && event.session &&
event.session.hasOwnProperty('_user') === false) {
err_str.push('login required');
}
}
// check for login requirement (must first login before proceed)
if (para === '_admin') {
// NOTE: this check will be passed automatically if caller the server
// that is, as internal API this check is bypassed automatically
// because 'event.session' likely does not exist
if (checker[para] === true && event.session) {
if (event.session.hasOwnProperty('_user') === false ||
!(event.session._user.account === 'admin' ||
event.session._user.control.groups.indexOf('admin') !== (-1))) {
err_str.push('admin login required');
}
}
}
// check for group specifications
if (para === '_groups' || para === '_permissions') {
//LOG.warn('checking group permission with session info:', l_name);
//LOG.warn(event.session, l_name);
// check if logined user matches the group
var groups = checker['_groups'] || [];
var permissions = checker['_permissions'] || [];
// see if we've a group match
if (event.session.hasOwnProperty('_user') === true &&
l_checkGroups(groups, event.session._user.control.groups) === false &&
l_checkGroups(permissions, event.session._user.control.permissions) === false) {
err_str.push('group-permission denied, no group permission to access event');
break;
}
}
continue;
}
var actual_type = (event.data[para] instanceof Array ? 'array' : typeof event.data[para]);
// get an array of valid types
var valid_types = checker[para];
if (valid_types instanceof Array === false) {
valid_types = [valid_types];
}
for (var i=0; i < valid_types.length; i++) {
var defined_type = valid_types[i];
if (typeof defined_type !== 'string') {
err_str.push('invalid type in checker: ' + defined_type);
continue;
}
// check if the type check is optional (will check for type only if parameter is provided)
if (defined_type.charAt(0) === '+') {
// skip optional parameters if not available
if (actual_type === 'undefined') {
break;
}
defined_type = defined_type.substring(1);
}
// type check is considered pass if at least one defined_type matches the actual
if (actual_type === defined_type) {
break;
}
}
// type is considered mismatched is none of the defined types can be found
if (i === valid_types.length) {
err_str.push('arg [' + para + '] expects type \'' + valid_types + '\', actual: ' + actual_type);
}
}
}
}
// parameter sent is incorrect
if (err_str.length > 0) {
if (Object.keys(event.data).length > 0) {
err_str.push('args: ' + JSON.stringify(event.data));
}
LOG.error(err_str, l_name);
var obj = {};
//obj[SR.Tags['UPDATE']] = SR.Tags['RES_ERROR'];
obj[SR.Tags['UPDATE']] = msgtype;
obj[SR.Tags['PARA']] = {err: err_str};
SR.EventManager.checkout(event, obj);
return;
}
// check if we should forward to another app server for execution
if (l_checkForward(msgtype, event) === true)
return;
// call event handler
UTIL.safeCall(l_handlers[msgtype], event, conn_name);
var check_eventdone = function () {
// check if already checkout
if (typeof event.id !== 'undefined') {
LOG.error('event still not checkout after: ' + SR.Settings.TIMEOUT_EVENTHANDLE + ' ms', l_name);
SR.EventManager.dropEvent(event);
}
};
// set timeout to check if event has been checked out
var timeout_trigger = setTimeout(check_eventdone, SR.Settings.TIMEOUT_EVENTHANDLE);
};
// if callback returns true then the response_type is registered, false means not registered
this.addResponder = function (response_type, callback) {
// check parameter are correct
if (typeof response_type !== 'string' || typeof callback !== 'function')
return undefined;
// create a unique ID for future communication
var cid = UTIL.createID();
// if not exist, insert new array already exist, then attach into array
if (l_responders.hasOwnProperty(response_type) === false)
l_responders[response_type] = [];
l_responders[response_type].push(
{
onResponse: callback,
cid: cid
}
);
// return unique key to be attached to msg to identify this communication
return cid;
};
//
// private methods
//
// TODO: move to some other user management place?
// check if groups match, return true if any from set1 matches any from set2
var l_checkGroups = function (set1, set2) {
if (set1 instanceof Array === false ||
set2 instanceof Array === false)
return false;
// return true if at least one match is found
for (var i=0; i < set1.length; i++) {
for (var j=0; j < set2.length; j++) {
if (set1[i] === set2[j]) {
//console.log("matched group/permission: " + set1[i]);
return true;
}
}
}
return false;
};
// notify that a response to a particular event is received
var l_handleEventResponse = function (response_type, event) {
LOG.sys('handling event response [' + response_type + ']', l_name);
// go over each registered callback function and see which one responds
for (var i=0; i < l_responders[response_type].length; i++) {
// find the callback with matching client id
// call callback and see whether it has been processed
if (l_responders[response_type][i].cid === event.cid) {
// log incoming message type & IP/port
LOG.sys(SR.Tags.RCV + response_type + ' from ' + event.printSource() + SR.Tags.END, l_name);
// make callback
UTIL.safeCall(l_responders[response_type][i].onResponse, event);
// then remove it
l_responders[response_type].splice(i, 1);
i--;
}
}
};
};