docs: 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--;
  			}
  		}
  	}
  }