<commit_before>#!/usr/bin/env node

var ws           = require ( "nodejs-websocket" ) ;
var EventEmitter = require ( "events" ).EventEmitter ;
var util         = require ( 'util' ) ;

var Event        = require ( "./Event" ) ;
var Client       = require ( "./Client" ) ;
var Lock         = require ( "./Lock" ) ;
var Semaphore    = require ( "./Semaphore" ) ;

var Log          = require ( "./LogFile" ) ;
var MultiHash    = require ( "./MultiHash" ) ;
var T            = require ( "./Tango" ) ;

/**
 * Description
 * @constructor
 * @method WebSocketEventProxy
 * @param {} port
 * @return
 */
var WebSocketEventProxy = function ( port )
{
  EventEmitter.call ( this ) ;
	this.className = "WebSocketEventProxy" ;
  this._sockets = {} ;
  this._eventNameToSocketContext = new MultiHash() ;
	this.client = null ;
	this.port = port ;
	this._create() ;
};
util.inherits ( WebSocketEventProxy, EventEmitter ) ;

/**
 * Description
 * @method toString
 * @return BinaryExpression
 */
WebSocketEventProxy.prototype.toString = function()
{
	return "(" + this.className + ")[port=" + this.port + "]" ;
};
/**
 * Description
 * @method closeAllWebsockets
 * @return
 */
WebSocketEventProxy.prototype.closeAllWebsockets = function()
{
	if ( ! this.server ) return ;

	this.server.connections.forEach(function (conn)
	{
    conn.close() ;
  }) ;
};
/**
 * Description
 * @method sendToWebSocket
 * @param {} e
 * @return
 */
WebSocketEventProxy.prototype.sendToWebSocket = function ( e )
{
	var pid = e.getProxyIdentifier() ;
	var conn = this._sockets[pid]
	if ( conn )
	{
		conn.send ( e ) ;
	}
};
/**
 * Description
 * @method generalEventListenerFunction
 * @param {} e
 * @return
 */
WebSocketEventProxy.prototype.generalEventListenerFunction = function ( e )
{
	var se = e.serialize() ;
	var i, conn ;
	var name = e.getName() ;
	var list = this._eventNameToSocketContext.get ( name ) ;
	if ( list )
	{
		for ( i = 0 ; i < list.length ; i++ )
		{
			conn = list[i] ;
			if ( conn.socket.key !== e.getProxyIdentifier() ) // TODO
			{
				conn.socket.sendText ( se ) ;
			}
		}
	}
};
/**
 * Description
 * @method removeWebsocket
 * @param {} socket
 * @return
 */
WebSocketEventProxy.prototype.removeWebsocket = function ( socket )
{
	var conn = this._sockets[socket.key] ;
	if ( ! conn )
	{
		return ;
	}
	conn.remove() ;
};
WebSocketEventProxy.prototype._create = function()
{
	var wssOptions = {} ;
	var thiz = this ;
	this.server = ws.createServer ( wssOptions, function ( socket )
	{
		var eventNameList ;
		var i = 0 ;
		var index = 0 ;

		Log.info ( 'web connects' ) ;
		var e ;
		socket.on ( "text", function ( message )
		{
			try
			{
				e = Event.prototype.deserialize ( message ) ;
			}
			catch ( exc )
			{
				Log.log ( exc ) ;
			}
			e.setProxyIdentifier ( socket.key ) ;
			var conn = thiz._sockets[this.key] ;
			if ( ! conn )
			{
				conn = new Conn  ( thiz, socket ) ;
			}
			if ( ! thiz.client )
			{
				thiz.client = new Client() ;
				thiz.client.on ( 'end', function()
				{
					// thiz.client.removeAllListeners() ;
				  thiz.client = null ;
					Log.info ( 'gepard connection closed.' ) ;
				});
				thiz.client.on ( 'shutdown', function()
				{
					// thiz.client.removeAllListeners() ;
					Log.info ( 'gepard shutdown.' ) ;
					thiz.closeAllWebsockets() ;
				});
			}
			if ( e.getName() === 'system' )
			{
				thiz.handleSystemMessages ( conn, e ) ;
			}
			else
			{
				if ( ! e.isResultRequested() )
				{
					e.control.__ignore_result_function_as_result_indicator__ = true ;
				}
				thiz.client.fire ( e
	      , {
/**
  * Description
  * @method result
  * @param {} e
  * @return
  */
 result: function(e)
	          {
							if ( e.isResult() )
							{
								thiz.sendToWebSocket ( e ) ;
							}
	          }
	        	,
/**
  * Description
  * @method error
  * @param {} e
  * @return
  */
 error: function(e)
	          	{
								thiz.sendToWebSocket ( e ) ;
	          	}
	         	,
/**
  * Description
  * @method write
  * @return
  */
 write: function()
	          	{
	          	}
	        	}) ;
				}
		}) ;
		socket.on ( "error", function ( e )
		{
			Log.info ( "web-socket closed with error" ) ;
			thiz.removeWebsocket ( this ) ;
		}) ;
		socket.on ( "close", function ( message )
		{
			Log.info ( "web-socket closed" ) ;
			thiz.removeWebsocket ( this ) ;
		}) ;
	}) ;
};
/**
 * Description
 * @method handleSystemMessages
 * @param {} conn
 * @param {} ne
 * @return
 */
WebSocketEventProxy.prototype.handleSystemMessages = function ( conn, e )
{
	if ( e.getType() === "client_info" )
	{
		e.setType ( "client_info_response" ) ;
		conn.send ( e ) ;
		return ;
	}
	if ( e.getType() === 'addEventListener' )
	{
	  eventNameList = e.body.eventNameList ;
	  var errText = "" ;
	  if ( ! eventNameList ) { Log.error ( "Missing eventNameList." ) ; return ; }
	  if ( typeof eventNameList === 'string' ) eventNameList = [ eventNameList ] ;
	  if ( ! Array.isArray ( eventNameList ) )
	  {
	    Log.error ( "eventNameList must be a string or an array of strings." ) ; return ;
	  }
	  if ( ! eventNameList.length )
	  {
	    Log.error ( "eventNameList must not be empty." ) ; return ;
	  }
	  if ( ! conn.eventNameList ) conn.eventNameList = [] ;
	  for ( i = 0 ; i < conn.eventNameList.lenth ; i++ )
	  {
	  	index = eventNameList.indexOf ( conn.eventNameList[i] ) ;
	  	if ( index >= 0 )
	  	{
        eventNameList.splice ( index, 1 ) ;
	  	}
	  }
	  if ( ! eventNameList.length )
	  {
	  	return ;
	  }
	  for ( i = 0 ; i < eventNameList.length ; i++ )
	  {
	  	conn.eventNameList.push ( eventNameList[i] ) ;
	  }
	  var eventNameListToBePropagated = [] ;
	  for ( i = 0 ; i < eventNameList.length ; i++ )
	  {
	  	if ( ! this._eventNameToSocketContext.get ( eventNameList[i] ) )
	  	{
	  		eventNameListToBePropagated.push ( eventNameList[i] ) ;
	  	}
			this._eventNameToSocketContext.put ( eventNameList[i], conn ) ;
	  }
	  if ( eventNameListToBePropagated.length )
	  {
	  	this.client.addEventListener ( eventNameListToBePropagated, this.generalEventListenerFunction.bind ( this ) ) ;
	  }
	}
	else
	if ( e.getType() === 'removeEventListener' )
	{
	  eventNameList = e.body.eventNameList ;
	  var errText = "" ;
	  if ( ! eventNameList ) { Log.error ( "Missing eventNameList." ) ; return ; }
	  if ( typeof eventNameList === 'string' ) eventNameList = [ eventNameList ] ;
	  if ( ! Array.isArray ( eventNameList ) )
	  {
	    Log.error ( "eventNameList must be a string or an array of strings." ) ; return ;
	  }
	  if ( ! eventNameList.length )
	  {
	    Log.error ( "eventNameList must not be empty." ) ; return ;
	  }
		var currentKeys = this._eventNameToSocketContext.getKeys() ;
		if ( conn.eventNameList )
		{
			for ( i = 0 ; i < eventNameList.length ; i++ )
			{
		  	index = conn.eventNameList.indexOf ( eventNameList[i] ) ;
		  	if ( index >= 0 )
		  	{
	        conn.eventNameList.splice ( index, 1 ) ;
					this._eventNameToSocketContext.remove ( eventNameList[i], conn ) ;
		  	}
			}
		}
	  var eventNamesToBeRemoved = [] ;
		for ( i = 0 ; i < currentKeys.length ; i++ )
		{
			if ( ! this._eventNameToSocketContext.get ( currentKeys[i] ) )
			{
				eventNamesToBeRemoved.push ( currentKeys[i] ) ;
			}
		}
		if ( eventNamesToBeRemoved.length )
		{
			this.client.removeEventListener ( eventNamesToBeRemoved ) ;
		}
	}
	else
	if ( e.getType() === 'lockResourceRequest' )
	{
		conn._lockResourceRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'unlockResourceRequest' )
	{
		conn._unlockResourceRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'acquireSemaphoreRequest' )
	{
		conn._acquireSemaphoreRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'releaseSemaphoreRequest' )
	{
		conn._releaseSemaphoreRequest  ( e ) ;
	}
  else
  {
    Log.error ( "Invalid event received:\n" + e ) ; return ;
  }
};
/**
 * Description
 * @method listen
 * @param {} port
 * @return
 */
WebSocketEventProxy.prototype.listen = function ( port )
{
	if ( port )
	{
		this.port = port ;
	}
	if ( ! this.port )
	{
    this.port = T.getProperty ( "gepard.websocket.port", 17502 ) ;
	}
	this.server.listen ( this.port, this.listenSocketBound.bind ( this ) ) ;
};
/**
 * Description
 * @method listenSocketBound
 * @return
 */
WebSocketEventProxy.prototype.listenSocketBound = function()
{
	Log.info ( "WebSocketEventProxy bound to port=" + this.port ) ;
};
WebSocketEventProxy.prototype.shutdown = function()
{
	if ( ! this.server ) return ;
	this.server.socket.close() ;
	this.server = null ;
};
/**
 * Internal class
 * @constructor
 */
var Conn = function ( proxy, socket )
{
	this.socket                   = socket ;
	this.sid                      = socket.key ;
	this.proxy                    = proxy ;
	this.proxy._sockets[this.sid] = this ;
	this.client                   = null ;
	this._locks                   = {} ;
	this._semaphores              = {} ;
};
Conn.prototype.toString = function()
{
  return "(Conn)[sid=" + this.sid +  "\n" + util.inspect ( this._locks  ) + "]" ;
};
Conn.prototype.getClient = function()
{
	if ( this.client )
	{
		return this.client ;
	}
	this.client = new Client() ;
	return this.client ;
};
Conn.prototype.send = function ( what )
{
	if ( what instanceof Event )
	{
		this.socket.sendText ( what.serialize() ) ;
		return ;
	}
	if ( typeof what === 'string' )
	{
		this.socket.sendText ( what ) ;
	}
};
Conn.prototype.remove = function()
{
	var eventNamesToBeRemoved = [] ;
	this.socket.removeAllListeners ( "text" ) ;
	this.socket.removeAllListeners ( "error" ) ;
	this.socket.removeAllListeners ( "close" ) ;

	var currentKeys = this.proxy._eventNameToSocketContext.getKeys() ;
	this.proxy._eventNameToSocketContext.remove ( this ) ;
	for ( i = 0 ; i < currentKeys.length ; i++ )
	{
		if ( ! this.proxy._eventNameToSocketContext.get ( currentKeys[i] ) )
		{
			eventNamesToBeRemoved.push ( currentKeys[i] ) ;
		}
	}
	delete this.proxy._sockets[this.sid] ;

	if ( eventNamesToBeRemoved.length && this.proxy.client )
	{
		this.proxy.client.removeEventListener ( eventNamesToBeRemoved ) ;
	}
	this.flush() ;
};
Conn.prototype.flush = function ()
{
	this.socket = null ;
	this.sid    = null ;
	this.proxy  = null ;
	if ( this.client )
	{
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
	}
	this._locks      = {} ;
	this._semaphores = {} ;
};
Conn.prototype._lockResourceRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	if ( this._locks[resourceId] )
	{
		return
	}
	var lock = new Lock ( resourceId, this.getClient() ) ;
	this._locks[resourceId] = lock ;
  lock.acquire ( function ( err )
  {
    e.setType ( "lockResourceResult" ) ;
  	if ( err )
  	{
  		e.body.isLockOwner = false ;
  	}
  	else
  	{
	  	e.body.isLockOwner = this.isOwner() ;
  	}
  	if ( ! e.body.isLockOwner )
  	{
			delete thiz._locks[resourceId] ;
  	}
  	thiz.send ( e ) ;
  } ) ;
};
Conn.prototype._unlockResourceRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	var lock = this._locks[resourceId] ;
	if ( ! lock )
	{
		return
	}
	delete this._locks[resourceId] ;
  lock.release() ;
  if ( ! this.client.holdsLocksOrSemaphores() )
  {
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
  }
};
Conn.prototype._acquireSemaphoreRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	if ( this._semaphores[resourceId] )
	{
		return
	}
	var sem = new Semaphore ( resourceId, this.getClient() ) ;
	this._semaphores[resourceId] = sem ;
  sem.acquire ( function ( err )
  {
    e.setType ( "acquireSemaphoreResult" ) ;
  	if ( err )
  	{
  		e.body.isSemaphoreOwner = false ;
  	}
  	else
  	{
	  	e.body.isSemaphoreOwner = this.isOwner() ;
  	}
  	thiz.send ( e ) ;
  } ) ;
};
Conn.prototype._releaseSemaphoreRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	var sem = this._semaphores[resourceId] ;
	if ( ! sem )
	{
		return
	}
	delete this._semaphores[resourceId] ;
  sem.release() ;
  if ( ! this.client.holdsLocksOrSemaphores() )
  {
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
  }
};

module.exports = WebSocketEventProxy ;

if ( require.main === module )
{
	var ep = new WebSocketEventProxy() ;
	var WEBSOCKET_PORT = T.getProperty ( "gepard.websocket.port", 17502 ) ;
	ep.listen ( WEBSOCKET_PORT ) ;
}

<commit_msg>remove useless comment, gepard.websocket.exclude.self=false|true<commit_after>#!/usr/bin/env node

var ws           = require ( "nodejs-websocket" ) ;
var EventEmitter = require ( "events" ).EventEmitter ;
var util         = require ( 'util' ) ;

var Event        = require ( "./Event" ) ;
var Client       = require ( "./Client" ) ;
var Lock         = require ( "./Lock" ) ;
var Semaphore    = require ( "./Semaphore" ) ;

var Log          = require ( "./LogFile" ) ;
var MultiHash    = require ( "./MultiHash" ) ;
var T            = require ( "./Tango" ) ;

/**
 * Description
 * @constructor
 * @method WebSocketEventProxy
 * @param {} port
 * @return
 */
var WebSocketEventProxy = function ( port )
{
  EventEmitter.call ( this ) ;
	this.className = "WebSocketEventProxy" ;
  this._sockets = {} ;
  this._eventNameToSocketContext = new MultiHash() ;
	this.client = null ;
	this.port = port ;
	this._create() ;
	this._excludeSelf = T.getBool ( "gepard.websocket.exclude.self", false ) ;
};
util.inherits ( WebSocketEventProxy, EventEmitter ) ;

/**
 * Description
 * @method toString
 * @return BinaryExpression
 */
WebSocketEventProxy.prototype.toString = function()
{
	return "(" + this.className + ")[port=" + this.port + "]" ;
};
/**
 * Description
 * @method closeAllWebsockets
 * @return
 */
WebSocketEventProxy.prototype.closeAllWebsockets = function()
{
	if ( ! this.server ) return ;

	this.server.connections.forEach(function (conn)
	{
    conn.close() ;
  }) ;
};
/**
 * Description
 * @method sendToWebSocket
 * @param {} e
 * @return
 */
WebSocketEventProxy.prototype.sendToWebSocket = function ( e )
{
	var pid = e.getProxyIdentifier() ;
	var conn = this._sockets[pid]
	if ( conn )
	{
		conn.send ( e ) ;
	}
};
/**
 * Description
 * @method generalEventListenerFunction
 * @param {} e
 * @return
 */
WebSocketEventProxy.prototype.generalEventListenerFunction = function ( e )
{
	var se = e.serialize() ;
	var i, conn ;
	var name = e.getName() ;
	var list = this._eventNameToSocketContext.get ( name ) ;
	if ( list )
	{
		for ( i = 0 ; i < list.length ; i++ )
		{
			conn = list[i] ;
			this._excludeSelf
			if ( this._excludeSelf && conn.socket.key === e.getProxyIdentifier() ) continue ;
			conn.socket.sendText ( se ) ;
		}
	}
};
/**
 * Description
 * @method removeWebsocket
 * @param {} socket
 * @return
 */
WebSocketEventProxy.prototype.removeWebsocket = function ( socket )
{
	var conn = this._sockets[socket.key] ;
	if ( ! conn )
	{
		return ;
	}
	conn.remove() ;
};
WebSocketEventProxy.prototype._create = function()
{
	var wssOptions = {} ;
	var thiz = this ;
	this.server = ws.createServer ( wssOptions, function ( socket )
	{
		var eventNameList ;
		var i = 0 ;
		var index = 0 ;

		Log.info ( 'web connects' ) ;
		var e ;
		socket.on ( "text", function ( message )
		{
			try
			{
				e = Event.prototype.deserialize ( message ) ;
			}
			catch ( exc )
			{
				Log.log ( exc ) ;
			}
			e.setProxyIdentifier ( socket.key ) ;
			var conn = thiz._sockets[this.key] ;
			if ( ! conn )
			{
				conn = new Conn  ( thiz, socket ) ;
			}
			if ( ! thiz.client )
			{
				thiz.client = new Client() ;
				thiz.client.on ( 'end', function()
				{
					// thiz.client.removeAllListeners() ;
				  thiz.client = null ;
					Log.info ( 'gepard connection closed.' ) ;
				});
				thiz.client.on ( 'shutdown', function()
				{
					// thiz.client.removeAllListeners() ;
					Log.info ( 'gepard shutdown.' ) ;
					thiz.closeAllWebsockets() ;
				});
			}
			if ( e.getName() === 'system' )
			{
				thiz.handleSystemMessages ( conn, e ) ;
			}
			else
			{
				if ( ! e.isResultRequested() )
				{
					e.control.__ignore_result_function_as_result_indicator__ = true ;
				}
				thiz.client.fire ( e
	      , {
						result: function(e)
	          {
							if ( e.isResult() )
							{
								thiz.sendToWebSocket ( e ) ;
							}
	          }
	        	,
						error: function(e)
	          {
							thiz.sendToWebSocket ( e ) ;
	          }
	         	,
						write: function()
	          {
	          }
	        }) ;
				}
		}) ;
		socket.on ( "error", function ( e )
		{
			Log.info ( "web-socket closed with error" ) ;
			thiz.removeWebsocket ( this ) ;
		}) ;
		socket.on ( "close", function ( message )
		{
			Log.info ( "web-socket closed" ) ;
			thiz.removeWebsocket ( this ) ;
		}) ;
	}) ;
};
/**
 * Description
 * @method handleSystemMessages
 * @param {} conn
 * @param {} ne
 * @return
 */
WebSocketEventProxy.prototype.handleSystemMessages = function ( conn, e )
{
	if ( e.getType() === "client_info" )
	{
		e.setType ( "client_info_response" ) ;
		conn.send ( e ) ;
		return ;
	}
	if ( e.getType() === 'addEventListener' )
	{
	  eventNameList = e.body.eventNameList ;
	  var errText = "" ;
	  if ( ! eventNameList ) { Log.error ( "Missing eventNameList." ) ; return ; }
	  if ( typeof eventNameList === 'string' ) eventNameList = [ eventNameList ] ;
	  if ( ! Array.isArray ( eventNameList ) )
	  {
	    Log.error ( "eventNameList must be a string or an array of strings." ) ; return ;
	  }
	  if ( ! eventNameList.length )
	  {
	    Log.error ( "eventNameList must not be empty." ) ; return ;
	  }
	  if ( ! conn.eventNameList ) conn.eventNameList = [] ;
	  for ( i = 0 ; i < conn.eventNameList.lenth ; i++ )
	  {
	  	index = eventNameList.indexOf ( conn.eventNameList[i] ) ;
	  	if ( index >= 0 )
	  	{
        eventNameList.splice ( index, 1 ) ;
	  	}
	  }
	  if ( ! eventNameList.length )
	  {
	  	return ;
	  }
	  for ( i = 0 ; i < eventNameList.length ; i++ )
	  {
	  	conn.eventNameList.push ( eventNameList[i] ) ;
	  }
	  var eventNameListToBePropagated = [] ;
	  for ( i = 0 ; i < eventNameList.length ; i++ )
	  {
	  	if ( ! this._eventNameToSocketContext.get ( eventNameList[i] ) )
	  	{
	  		eventNameListToBePropagated.push ( eventNameList[i] ) ;
	  	}
			this._eventNameToSocketContext.put ( eventNameList[i], conn ) ;
	  }
	  if ( eventNameListToBePropagated.length )
	  {
	  	this.client.addEventListener ( eventNameListToBePropagated, this.generalEventListenerFunction.bind ( this ) ) ;
	  }
	}
	else
	if ( e.getType() === 'removeEventListener' )
	{
	  eventNameList = e.body.eventNameList ;
	  var errText = "" ;
	  if ( ! eventNameList ) { Log.error ( "Missing eventNameList." ) ; return ; }
	  if ( typeof eventNameList === 'string' ) eventNameList = [ eventNameList ] ;
	  if ( ! Array.isArray ( eventNameList ) )
	  {
	    Log.error ( "eventNameList must be a string or an array of strings." ) ; return ;
	  }
	  if ( ! eventNameList.length )
	  {
	    Log.error ( "eventNameList must not be empty." ) ; return ;
	  }
		var currentKeys = this._eventNameToSocketContext.getKeys() ;
		if ( conn.eventNameList )
		{
			for ( i = 0 ; i < eventNameList.length ; i++ )
			{
		  	index = conn.eventNameList.indexOf ( eventNameList[i] ) ;
		  	if ( index >= 0 )
		  	{
	        conn.eventNameList.splice ( index, 1 ) ;
					this._eventNameToSocketContext.remove ( eventNameList[i], conn ) ;
		  	}
			}
		}
	  var eventNamesToBeRemoved = [] ;
		for ( i = 0 ; i < currentKeys.length ; i++ )
		{
			if ( ! this._eventNameToSocketContext.get ( currentKeys[i] ) )
			{
				eventNamesToBeRemoved.push ( currentKeys[i] ) ;
			}
		}
		if ( eventNamesToBeRemoved.length )
		{
			this.client.removeEventListener ( eventNamesToBeRemoved ) ;
		}
	}
	else
	if ( e.getType() === 'lockResourceRequest' )
	{
		conn._lockResourceRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'unlockResourceRequest' )
	{
		conn._unlockResourceRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'acquireSemaphoreRequest' )
	{
		conn._acquireSemaphoreRequest  ( e ) ;
	}
	else
	if ( e.getType() === 'releaseSemaphoreRequest' )
	{
		conn._releaseSemaphoreRequest  ( e ) ;
	}
  else
  {
    Log.error ( "Invalid event received:\n" + e ) ; return ;
  }
};
/**
 * Description
 * @method listen
 * @param {} port
 * @return
 */
WebSocketEventProxy.prototype.listen = function ( port )
{
	if ( port )
	{
		this.port = port ;
	}
	if ( ! this.port )
	{
    this.port = T.getProperty ( "gepard.websocket.port", 17502 ) ;
	}
	this.server.listen ( this.port, this.listenSocketBound.bind ( this ) ) ;
};
/**
 * Description
 * @method listenSocketBound
 * @return
 */
WebSocketEventProxy.prototype.listenSocketBound = function()
{
	Log.info ( "WebSocketEventProxy bound to port=" + this.port ) ;
};
WebSocketEventProxy.prototype.shutdown = function()
{
	if ( ! this.server ) return ;
	this.server.socket.close() ;
	this.server = null ;
};
/**
 * Internal class
 * @constructor
 */
var Conn = function ( proxy, socket )
{
	this.socket                   = socket ;
	this.sid                      = socket.key ;
	this.proxy                    = proxy ;
	this.proxy._sockets[this.sid] = this ;
	this.client                   = null ;
	this._locks                   = {} ;
	this._semaphores              = {} ;
};
Conn.prototype.toString = function()
{
  return "(Conn)[sid=" + this.sid +  "\n" + util.inspect ( this._locks  ) + "]" ;
};
Conn.prototype.getClient = function()
{
	if ( this.client )
	{
		return this.client ;
	}
	this.client = new Client() ;
	return this.client ;
};
Conn.prototype.send = function ( what )
{
	if ( what instanceof Event )
	{
		this.socket.sendText ( what.serialize() ) ;
		return ;
	}
	if ( typeof what === 'string' )
	{
		this.socket.sendText ( what ) ;
	}
};
Conn.prototype.remove = function()
{
	var eventNamesToBeRemoved = [] ;
	this.socket.removeAllListeners ( "text" ) ;
	this.socket.removeAllListeners ( "error" ) ;
	this.socket.removeAllListeners ( "close" ) ;

	var currentKeys = this.proxy._eventNameToSocketContext.getKeys() ;
	this.proxy._eventNameToSocketContext.remove ( this ) ;
	for ( i = 0 ; i < currentKeys.length ; i++ )
	{
		if ( ! this.proxy._eventNameToSocketContext.get ( currentKeys[i] ) )
		{
			eventNamesToBeRemoved.push ( currentKeys[i] ) ;
		}
	}
	delete this.proxy._sockets[this.sid] ;

	if ( eventNamesToBeRemoved.length && this.proxy.client )
	{
		this.proxy.client.removeEventListener ( eventNamesToBeRemoved ) ;
	}
	this.flush() ;
};
Conn.prototype.flush = function ()
{
	this.socket = null ;
	this.sid    = null ;
	this.proxy  = null ;
	if ( this.client )
	{
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
	}
	this._locks      = {} ;
	this._semaphores = {} ;
};
Conn.prototype._lockResourceRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	if ( this._locks[resourceId] )
	{
		return
	}
	var lock = new Lock ( resourceId, this.getClient() ) ;
	this._locks[resourceId] = lock ;
  lock.acquire ( function ( err )
  {
    e.setType ( "lockResourceResult" ) ;
  	if ( err )
  	{
  		e.body.isLockOwner = false ;
  	}
  	else
  	{
	  	e.body.isLockOwner = this.isOwner() ;
  	}
  	if ( ! e.body.isLockOwner )
  	{
			delete thiz._locks[resourceId] ;
  	}
  	thiz.send ( e ) ;
  } ) ;
};
Conn.prototype._unlockResourceRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	var lock = this._locks[resourceId] ;
	if ( ! lock )
	{
		return
	}
	delete this._locks[resourceId] ;
  lock.release() ;
  if ( ! this.client.holdsLocksOrSemaphores() )
  {
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
  }
};
Conn.prototype._acquireSemaphoreRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	if ( this._semaphores[resourceId] )
	{
		return
	}
	var sem = new Semaphore ( resourceId, this.getClient() ) ;
	this._semaphores[resourceId] = sem ;
  sem.acquire ( function ( err )
  {
    e.setType ( "acquireSemaphoreResult" ) ;
  	if ( err )
  	{
  		e.body.isSemaphoreOwner = false ;
  	}
  	else
  	{
	  	e.body.isSemaphoreOwner = this.isOwner() ;
  	}
  	thiz.send ( e ) ;
  } ) ;
};
Conn.prototype._releaseSemaphoreRequest = function ( e )
{
	var thiz = this ;
	var resourceId = e.body.resourceId  ;
	if ( ! resourceId ) return ;
	var sem = this._semaphores[resourceId] ;
	if ( ! sem )
	{
		return
	}
	delete this._semaphores[resourceId] ;
  sem.release() ;
  if ( ! this.client.holdsLocksOrSemaphores() )
  {
		try
		{
			this.client.end() ;
		}
		catch ( exc )
		{
		}
		this.client = null ;
  }
};

module.exports = WebSocketEventProxy ;

if ( require.main === module )
{
	var ep = new WebSocketEventProxy() ;
	var WEBSOCKET_PORT = T.getProperty ( "gepard.websocket.port", 17502 ) ;
	ep.listen ( WEBSOCKET_PORT ) ;
}

<|endoftext|>


<commit_before>// Copyright 2015, University of Colorado Boulder

/**
 * Tandem is a general instance registry that can be used to track creation/disposal of instances in PhET Simulations.
 * It is used for phetio.js instrumentation for PhET-iO support.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var tandemNamespace = require( 'TANDEM/tandemNamespace' );
  var toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  var packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  var packageJSON = JSON.parse( packageString );

  // variables
  var launched = false;

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @param {Object} [options]
   * @constructor
   */
  function Tandem( id, options ) {

    options = _.extend( {
      static: false,
      enabled: true,
      tandemRequiredButNotSupplied: false, // When set to true, if there isn't a tandem supplied, it will fail out.
      tandemOptionalAndNotSupplied: false
    }, options );

    // @public (read-only)
    this.id = ( id !== undefined ) ? id : '';

    // @private (read-only)
    this.static = options.static;
    this.tandemRequiredButNotSupplied = options.tandemRequiredButNotSupplied;
    this.tandemOptionalAndNotSupplied = options.tandemOptionalAndNotSupplied;
    this.enabled = options.enabled;
  }

  var staticInstances = [];

  tandemNamespace.register( 'Tandem', Tandem );

  // Listeners that will be notified when items are registered/deregistered
  var instanceListeners = [];

  inherit( Object, Tandem, {

    // @public (read-only) permit checking whether something is probably a Tandem instance for error checking
    isTandem: true,

    /**
     * Adds an instance of any type.  For example, it could be an axon Property, scenery Node or Sun button.  Each
     * item should only be added to the registry once, but that is not enforced here in Tandem.  For PhET-iO, phet-io.js
     * enforces one entry per ID in phetio.addInstance
     *
     * This is used to register instances with PhET-iO.
     * @param {Object} instance - the instance to add
     * @param {function} type - the PhET-iO type function, such as TString or TProperty(TNumber('volts'))
     * @public
     */
    addInstance: function( instance, type ) {

      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' && this.enabled ) {

        assert && assert( !this.tandemRequiredButNotSupplied, 'Tandem was required but not supplied' );

        if ( !type ) {
          console.log( 'Missing type declaration for ' + this.id );
        }

        // ifphetio returns a no-op function, so to test whether a valid T wrapper type was passed, we search for the typeName
        assert && assert( type && type.typeName, 'type must be specified and have a typeName' );

        if ( this.tandemOptionalAndNotSupplied ) {
          return; // For optionally instrumented types that are not provided tandems, the instance isn't really "added"
                  // but likewise, it in not an error
        }

        if ( this.static && !launched ) {
          staticInstances.push( { tandem: this, instance: instance, type: type } );
        }
        else {
          for ( var i = 0; i < instanceListeners.length; i++ ) {
            instanceListeners[ i ].addInstance( this.id, instance, type );
          }
        }
      }
    },

    /**
     * Removes an instance from the
     * @param {Object} instance - the instance to remove
     * @public
     */
    removeInstance: function( instance ) {

      // Only active when running as phet-io
      if ( phet.chipper.brand === 'phet-io' && this.enabled ) {
        for ( var i = 0; i < instanceListeners.length; i++ ) {
          instanceListeners[ i ].removeInstance( this.id, instance );
        }
      }
    },

    /**
     * Create a new Tandem by appending the given id
     * @param {string} id
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem: function( id, options ) {

      // Make sure the id was provided
      assert && assert( typeof id === 'string' && id.length > 0, 'id must be defined' );

      var string = ( this.id.length > 0 ) ? ( this.id + '.' + id ) : id;

      // Any child of something static is also static
      options = _.extend( { static: this.static, enabled: this.enabled }, options );

      return new Tandem( string, options );
    },

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electron_0
     * sim.screen.model.electron_1
     *
     * In this case, 'sim.screen.model.electron' is the group tandem id.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param id
     * @returns {GroupTandem}
     */
    createGroupTandem: function( id ) {

      // Unfortunately we must resort to globals here since loading through the namespace would create a cycle
      return new GroupTandem( this.id + '.' + id );
    },

    /**
     * Get the last part of the tandem (after the last .), used in Joist for creating button names dynamically based
     * on screen names
     * @return {string} the tail of the tandem
     */
    get tail() {
      assert && assert( this.id.indexOf( '.' ) >= 0, 'tandem ID does not have a tail' );

      var lastIndexOfDot = this.id.lastIndexOf( '.' );
      var tail = this.id.substring( lastIndexOfDot + 1 );
      assert && assert( tail.length > 0, 'tandem ID did not have a tail' );
      return tail;
    },

    /**
     * Returns a Tandem for everything except the tail.
     * @returns {Tandem}
     */
    get parentTandem() {
      assert && assert( this.id.indexOf( '.' ) >= 0, 'tandem ID does not have a tail' );

      var lastIndexOfDot = this.id.lastIndexOf( '.' );
      var headID = this.id.substring( 0, lastIndexOfDot );

      return new Tandem( headID, {
        static: this.static,
        tandemRequiredButNotSupplied: this.tandemRequiredButNotSupplied,
        enabled: this.enabled
      } );
    },

    /**
     * When using subtyping, the instance listeners must only be notified once rather than once for every level
     * in the inheritance hierarchy.  When a subtype constructor has a tandem.addInstance call, it should
     * pass a supertype tandem to the parent constructor so that it won't try to register the item twice.
     * @returns {SupertypeTandem}
     */
    createSupertypeTandem: function() {
      return new SupertypeTandem( this.id );
    }
  }, {

    /**
     * Some common code (such as CheckBox or RadioButton) must always be instrumented and hence requires a tandem to be
     * passed in.  The options hash indicates this with {tandem: Tandem.tandemRequired()}
     * @returns {Tandem}
     */
    tandemRequired: function() {
      return rootTandem.createTandem( 'requiredTandem', { // TODO: for partially instrumented sims, should we add a numeric counter, like requiredTandem12
        tandemRequiredButNotSupplied: true // will be checked in addInstance
      } );
    },

    /**
     * Some common code (such as ObservableArray) must always be instrumented and hence requires a tandem to be
     * passed in.  The options hash indicates this with {tandem: Tandem.tandemRequired()}
     * @returns {Tandem}
     */
    tandemOptional: function() {
      return rootTandem.createTandem( 'optionalTandem', { // TODO: for partially instrumented sims, should we add a numeric counter, like requiredTandem12
        tandemOptionalAndNotSupplied: true
      } );
    },

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addInstance(id,instance),
     *   removeInstance(id,instance)
     * }
     * where id is of type {string} and instance is of type {Object}
     *
     * @param {Object} instanceListener - described above
     * @public
     * @static
     */
    addInstanceListener: function( instanceListener ) {
      instanceListeners.push( instanceListener );
    },

    /**
     * Create a tandem based on the name of the running simulation.
     * @returns {Tandem}
     */
    createRootTandem: function() {
      return new Tandem( toCamelCase( packageJSON.name ) );
    },

    /**
     * Create a child of the root tandem.
     * @param {string} name
     * @returns {Tandem}
     */
    createStaticTandem: function( name ) {
      return Tandem.createRootTandem().createTandem( name, { static: true } );
    },

    /**
     * When the simulation is launched, all static instances are registered.
     */
    launch: function() {
      assert && assert( !launched, 'Tandem was launched twice' );
      launched = true;
      while ( staticInstances.length > 0 ) {
        var staticInstance = staticInstances.shift();
        staticInstance.tandem.addInstance( staticInstance.instance, staticInstance.type );
      }
    },

    /**
     * Catch cases where tandem is being supplied to a class that doesn't support tandem.
     * @param options
     */
    disallowTandem: function( options ) {

      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' &&
           phet.phetio && phet.phetio.queryParameters && phet.phetio.queryParameters.phetioValidateTandems ) {
        assert && assert( !options.tandem, 'tandem is not allowed' );
      }
    },

    /**
     * When running in PhET-iO brand, some code (such as user interface components) must be instrumented for PhET-iO.
     * Uninstrumented files should call this function to indicate they still need to be instrumented, so they aren't
     * missed.  See https://github.com/phetsims/phet-io/issues/668
     */
    indicateUninstrumentedCode: function() {
      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' &&
           phet.phetio && phet.phetio.queryParameters && phet.phetio.queryParameters.phetioValidateTandems ) {
        assert && assert( false, 'Uninstrumented code detected' );
      }
    }
  } );

  var rootTandem = Tandem.createRootTandem();

  // Tandem checks for listeners added before the Tandem module was loaded.  This is necessary so that phetio.js can
  // receive notifications about items created during static initialization such as Solute.js
  // which is created before Sim.js runs.
  if ( window.tandemPreloadInstanceListeners ) {
    for ( var i = 0; i < window.tandemPreloadInstanceListeners.length; i++ ) {
      Tandem.addInstanceListener( window.tandemPreloadInstanceListeners[ i ] );
    }
  }

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createGroupTandem
   * Declared in the same file to avoid circular reference errors in module loading.
   */
  function GroupTandem( id ) {

    Tandem.call( this, id );

    // @private for generating indices from a pool
    this.groupElementIndex = 0;
  }

  tandemNamespace.register( 'Tandem.GroupTandem', GroupTandem );

  inherit( Tandem, GroupTandem, {

    createNextTandem: function() {
      return new Tandem( this.id + '_' + ( this.groupElementIndex++ ) );
    }
  } );

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createSupertypeTandem
   */
  function SupertypeTandem( id ) {
    Tandem.call( this, id );
  }

  tandemNamespace.register( 'Tandem.SupertypeTandem', SupertypeTandem );

  inherit( Tandem, SupertypeTandem, {

    // @public - Override to make no-op, see createSupertypeTandem
    addInstance: function( instance, type ) {},

    // @public - Override to make no-op, see createSupertypeTandem
    removeInstance: function( instance ) {}
  } );

  return Tandem;
} );

<commit_msg>Allow tandems to be skipped when phetioValidateTandems=false, see https://github.com/phetsims/john-travoltage/issues/182<commit_after>// Copyright 2015, University of Colorado Boulder

/**
 * Tandem is a general instance registry that can be used to track creation/disposal of instances in PhET Simulations.
 * It is used for phetio.js instrumentation for PhET-iO support.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var tandemNamespace = require( 'TANDEM/tandemNamespace' );
  var toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  var packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  var packageJSON = JSON.parse( packageString );

  // variables
  var launched = false;

  // increment names of required tandems to avoid collisions for uninstrumented sims with phetioValidateTandems=false
  var requiredTandemIndex = 0;

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @param {Object} [options]
   * @constructor
   */
  function Tandem( id, options ) {

    options = _.extend( {
      static: false,
      enabled: true,
      tandemRequiredButNotSupplied: false, // When set to true, if there isn't a tandem supplied, it will fail out.
      tandemOptionalAndNotSupplied: false
    }, options );

    // @public (read-only)
    this.id = ( id !== undefined ) ? id : '';

    // @private (read-only)
    this.static = options.static;
    this.tandemRequiredButNotSupplied = options.tandemRequiredButNotSupplied;
    this.tandemOptionalAndNotSupplied = options.tandemOptionalAndNotSupplied;
    this.enabled = options.enabled;
  }

  var staticInstances = [];

  tandemNamespace.register( 'Tandem', Tandem );

  // Listeners that will be notified when items are registered/deregistered
  var instanceListeners = [];

  inherit( Object, Tandem, {

    // @public (read-only) permit checking whether something is probably a Tandem instance for error checking
    isTandem: true,

    /**
     * Adds an instance of any type.  For example, it could be an axon Property, scenery Node or Sun button.  Each
     * item should only be added to the registry once, but that is not enforced here in Tandem.  For PhET-iO, phet-io.js
     * enforces one entry per ID in phetio.addInstance
     *
     * This is used to register instances with PhET-iO.
     * @param {Object} instance - the instance to add
     * @param {function} type - the PhET-iO type function, such as TString or TProperty(TNumber('volts'))
     * @public
     */
    addInstance: function( instance, type ) {

      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' && this.enabled ) {

        if ( phet.phetio.queryParameters.phetioValidateTandems ) {
          assert && assert( !this.tandemRequiredButNotSupplied, 'Tandem was required but not supplied' );
        }

        if ( !type ) {
          console.log( 'Missing type declaration for ' + this.id );
        }

        // ifphetio returns a no-op function, so to test whether a valid T wrapper type was passed, we search for the typeName
        assert && assert( type && type.typeName, 'type must be specified and have a typeName' );

        if ( this.tandemOptionalAndNotSupplied ) {
          return; // For optionally instrumented types that are not provided tandems, the instance isn't really "added"
                  // but likewise, it in not an error
        }

        if ( this.static && !launched ) {
          staticInstances.push( { tandem: this, instance: instance, type: type } );
        }
        else {
          for ( var i = 0; i < instanceListeners.length; i++ ) {
            instanceListeners[ i ].addInstance( this.id, instance, type );
          }
        }
      }
    },

    /**
     * Removes an instance from the
     * @param {Object} instance - the instance to remove
     * @public
     */
    removeInstance: function( instance ) {

      // Only active when running as phet-io
      if ( phet.chipper.brand === 'phet-io' && this.enabled ) {
        for ( var i = 0; i < instanceListeners.length; i++ ) {
          instanceListeners[ i ].removeInstance( this.id, instance );
        }
      }
    },

    /**
     * Create a new Tandem by appending the given id
     * @param {string} id
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem: function( id, options ) {

      // Make sure the id was provided
      assert && assert( typeof id === 'string' && id.length > 0, 'id must be defined' );

      var string = ( this.id.length > 0 ) ? ( this.id + '.' + id ) : id;

      // Any child of something static is also static
      options = _.extend( { static: this.static, enabled: this.enabled }, options );

      return new Tandem( string, options );
    },

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electron_0
     * sim.screen.model.electron_1
     *
     * In this case, 'sim.screen.model.electron' is the group tandem id.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param id
     * @returns {GroupTandem}
     */
    createGroupTandem: function( id ) {

      // Unfortunately we must resort to globals here since loading through the namespace would create a cycle
      return new GroupTandem( this.id + '.' + id );
    },

    /**
     * Get the last part of the tandem (after the last .), used in Joist for creating button names dynamically based
     * on screen names
     * @return {string} the tail of the tandem
     */
    get tail() {
      assert && assert( this.id.indexOf( '.' ) >= 0, 'tandem ID does not have a tail' );

      var lastIndexOfDot = this.id.lastIndexOf( '.' );
      var tail = this.id.substring( lastIndexOfDot + 1 );
      assert && assert( tail.length > 0, 'tandem ID did not have a tail' );
      return tail;
    },

    /**
     * Returns a Tandem for everything except the tail.
     * @returns {Tandem}
     */
    get parentTandem() {
      assert && assert( this.id.indexOf( '.' ) >= 0, 'tandem ID does not have a tail' );

      var lastIndexOfDot = this.id.lastIndexOf( '.' );
      var headID = this.id.substring( 0, lastIndexOfDot );

      return new Tandem( headID, {
        static: this.static,
        tandemRequiredButNotSupplied: this.tandemRequiredButNotSupplied,
        enabled: this.enabled
      } );
    },

    /**
     * When using subtyping, the instance listeners must only be notified once rather than once for every level
     * in the inheritance hierarchy.  When a subtype constructor has a tandem.addInstance call, it should
     * pass a supertype tandem to the parent constructor so that it won't try to register the item twice.
     * @returns {SupertypeTandem}
     */
    createSupertypeTandem: function() {
      return new SupertypeTandem( this.id );
    }
  }, {

    /**
     * Some common code (such as CheckBox or RadioButton) must always be instrumented and hence requires a tandem to be
     * passed in.  The options hash indicates this with {tandem: Tandem.tandemRequired()}
     * @returns {Tandem}
     */
    tandemRequired: function() {
      return rootTandem.createTandem( 'requiredTandem' + (requiredTandemIndex++), { // TODO: for partially instrumented sims, should we add a numeric counter, like requiredTandem12
        tandemRequiredButNotSupplied: true // will be checked in addInstance
      } );
    },

    /**
     * Some common code (such as ObservableArray) must always be instrumented and hence requires a tandem to be
     * passed in.  The options hash indicates this with {tandem: Tandem.tandemRequired()}
     * @returns {Tandem}
     */
    tandemOptional: function() {
      return rootTandem.createTandem( 'optionalTandem', { // TODO: for partially instrumented sims, should we add a numeric counter, like requiredTandem12
        tandemOptionalAndNotSupplied: true
      } );
    },

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addInstance(id,instance),
     *   removeInstance(id,instance)
     * }
     * where id is of type {string} and instance is of type {Object}
     *
     * @param {Object} instanceListener - described above
     * @public
     * @static
     */
    addInstanceListener: function( instanceListener ) {
      instanceListeners.push( instanceListener );
    },

    /**
     * Create a tandem based on the name of the running simulation.
     * @returns {Tandem}
     */
    createRootTandem: function() {
      return new Tandem( toCamelCase( packageJSON.name ) );
    },

    /**
     * Create a child of the root tandem.
     * @param {string} name
     * @returns {Tandem}
     */
    createStaticTandem: function( name ) {
      return Tandem.createRootTandem().createTandem( name, { static: true } );
    },

    /**
     * When the simulation is launched, all static instances are registered.
     */
    launch: function() {
      assert && assert( !launched, 'Tandem was launched twice' );
      launched = true;
      while ( staticInstances.length > 0 ) {
        var staticInstance = staticInstances.shift();
        staticInstance.tandem.addInstance( staticInstance.instance, staticInstance.type );
      }
    },

    /**
     * Catch cases where tandem is being supplied to a class that doesn't support tandem.
     * @param options
     */
    disallowTandem: function( options ) {

      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' &&
           phet.phetio && phet.phetio.queryParameters && phet.phetio.queryParameters.phetioValidateTandems ) {
        assert && assert( !options.tandem, 'tandem is not allowed' );
      }
    },

    /**
     * When running in PhET-iO brand, some code (such as user interface components) must be instrumented for PhET-iO.
     * Uninstrumented files should call this function to indicate they still need to be instrumented, so they aren't
     * missed.  See https://github.com/phetsims/phet-io/issues/668
     */
    indicateUninstrumentedCode: function() {
      if ( window.phet && window.phet.chipper && phet.chipper.brand === 'phet-io' &&
           phet.phetio && phet.phetio.queryParameters && phet.phetio.queryParameters.phetioValidateTandems ) {
        assert && assert( false, 'Uninstrumented code detected' );
      }
    }
  } );

  var rootTandem = Tandem.createRootTandem();

  // Tandem checks for listeners added before the Tandem module was loaded.  This is necessary so that phetio.js can
  // receive notifications about items created during static initialization such as Solute.js
  // which is created before Sim.js runs.
  if ( window.tandemPreloadInstanceListeners ) {
    for ( var i = 0; i < window.tandemPreloadInstanceListeners.length; i++ ) {
      Tandem.addInstanceListener( window.tandemPreloadInstanceListeners[ i ] );
    }
  }

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createGroupTandem
   * Declared in the same file to avoid circular reference errors in module loading.
   */
  function GroupTandem( id ) {

    Tandem.call( this, id );

    // @private for generating indices from a pool
    this.groupElementIndex = 0;
  }

  tandemNamespace.register( 'Tandem.GroupTandem', GroupTandem );

  inherit( Tandem, GroupTandem, {

    createNextTandem: function() {
      return new Tandem( this.id + '_' + ( this.groupElementIndex++ ) );
    }
  } );

  /**
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createSupertypeTandem
   */
  function SupertypeTandem( id ) {
    Tandem.call( this, id );
  }

  tandemNamespace.register( 'Tandem.SupertypeTandem', SupertypeTandem );

  inherit( Tandem, SupertypeTandem, {

    // @public - Override to make no-op, see createSupertypeTandem
    addInstance: function( instance, type ) {},

    // @public - Override to make no-op, see createSupertypeTandem
    removeInstance: function( instance ) {}
  } );

  return Tandem;
} );

<|endoftext|>


<commit_before>import Renderer from "./renderer.js";
import GenomeBrowserUtils from "../genome-browser-utils.js";
import {SVG} from "../../core/svg.js";

export default class VariantRenderer extends Renderer {

    render(features, options) {
        (features || []).forEach(feature => {
            const group = SVG.addChild(options.svgCanvasFeatures, "g", {});

            // get feature genomic information
            const start = feature.start;
            const end = feature.end;
            const length = Math.max(Math.abs((end - start) + 1), 1);

            // transform to pixel position
            const width = Math.max(length * options.pixelBase, 1);
            const x = GenomeBrowserUtils.getFeatureX(start, options);

            // Get variant info
            const variantColor = this.getValueFromConfig("variantColor", [feature]);
            const variantTooltipTitle = this.getValueFromConfig("variantTooltipTitle", [feature]);
            const variantTooltipText = this.getValueFromConfig("variantTooltipText", [feature]);

            // Render feature position
            const variantElement = SVG.addChild(group, "rect", {
                "x": x,
                "y": 0,
                "width": `${width}px`,
                "height": `${this.config.sampleHeaderHeight - this.config.sampleHeaderDividerHeight - 4}px`,
                // "stroke": "#000000",
                // "stroke-width": 1,
                // "stroke-opacity": 0.7,
                "fill": variantColor,
                "cursor": "pointer",
            });

            if (variantTooltipText) {
                $(variantElement).qtip({
                    content: {
                        text: variantTooltipText,
                        title: variantTooltipTitle || null,
                    },
                    position: {
                        viewport: window,
                        target: "mouse",
                    },
                    style: {
                        width: true,
                        classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                    },
                });
            }

            // Render for each sample in feature.studies[0].samples
            // this.config.sampleNames.forEach((sampleName, index) => {
            (feature.studies[0]?.samples || []).forEach((sampleData, index) => {
                // Only one study is expected, and GT is always the first field in samplesData
                const genotype = sampleData.data[0];
                const sampleGenotypeColor = this.getValueFromConfig("sampleGenotypeColor", [genotype]);
                const sampleGenotypeTooltipTitle = this.getValueFromConfig("sampleGenotypeTooltipTitle", [feature, sampleData]);
                const sampleGenotypeTooltipText = this.getValueFromConfig("sampleGenotypeTooltipText", [feature, sampleData]);

                const sampleGenotypeElement = SVG.addChild(group, "rect", {
                    "x": x,
                    "y": this.config.sampleHeaderHeight + (index * this.config.sampleHeight),
                    "width": width,
                    "height": this.config.sampleHeight - 2,
                    // "stroke": "#000000",
                    // "stroke-width": 1,
                    // "stroke-opacity": 0.7,
                    "fill": sampleGenotypeColor,
                    "cursor": "pointer",
                    // "data-genotype": genotype,
                });

                if (sampleGenotypeTooltipText) {
                    $(sampleGenotypeElement).qtip({
                        content: {
                            text: sampleGenotypeTooltipText,
                            title: sampleGenotypeTooltipTitle || null,
                        },
                        position: {
                            viewport: window,
                            target: "mouse",
                        },
                        style: {
                            width: true,
                            classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                        },
                    });
                }
            });
        });
    }

    getDefaultConfig() {
        return {
            infoWidgetId: "id",
            strokeColor: "#555",
            height: 10,
            histogramColor: "#58f3f0",
            // label: GenomeBrowserUtils.variantLabelFormatter,
            variantColor: GenomeBrowserUtils.variantColorFormatter,
            variantTooltipTitle: GenomeBrowserUtils.variantTooltipTitleFormatter,
            variantTooltipText: GenomeBrowserUtils.variantTooltipTextFormatter,
            sampleGenotypeColor: GenomeBrowserUtils.genotypeColorFormatter,
            sampleGenotypeTooltipTitle: GenomeBrowserUtils.sampleGenotypeTooltipTitleFormatter,
            sampleGenotypeTooltipText: GenomeBrowserUtils.sampleGenotypeTooltipTextFormatter,
            // sampleTrackHeight: 20,
            // sampleTrackY: 15,
            // sampleTrackFontSize: 12,
            sampleNames: [],
            sampleHeight: 20,
        };
    }

}
<commit_msg>gb: add lollipop visualization for variants #TASK-636<commit_after>import Renderer from "./renderer.js";
import GenomeBrowserUtils from "../genome-browser-utils.js";
import {SVG} from "../../core/svg.js";
import LollipopLayout from "../../core/visualisation/lollipop-layout.js";

export default class VariantRenderer extends Renderer {

    constructor(config) {
        super(config);
        this.lollipopLayout = new LollipopLayout({
            maxWidth: this.config.lollipopMaxWidth,
        });
    }

    render(features, options) {
        const topPosition = this.config.lollipopVisible ? this.config.lollipopHeight : this.config.headerHeight;
        let lollipopPositions = [];
        const lollipopRegionWidth = options.requestedRegion.length() * options.pixelBase;
        const lollipopStartX = GenomeBrowserUtils.getFeatureX(options.requestedRegion.start, options);
        // const lollipopEndX = GenomeBrowserUtils.getFeatureX(options.requestedRegion.end, options);

        if (this.config.lollipopVisible) {
            lollipopPositions = this.lollipopLayout.layout(features || [], options.requestedRegion, lollipopRegionWidth);
        }

        (features || []).forEach((feature, featureIndex) => {
            const group = SVG.addChild(options.svgCanvasFeatures, "g", {});

            // get feature genomic information
            const start = feature.start;
            const end = feature.end;
            const length = Math.max(Math.abs((end - start) + 1), 1);

            // transform to pixel position
            const width = Math.max(length * options.pixelBase, 1);
            const x = GenomeBrowserUtils.getFeatureX(start, options);

            // Get variant information
            const variantColor = this.getValueFromConfig("variantColor", [feature]);
            const variantTooltipTitle = this.getValueFromConfig("variantTooltipTitle", [feature]);
            const variantTooltipText = this.getValueFromConfig("variantTooltipText", [feature]);

            let variantElement = null;

            // Check if lollipops are visible
            if (this.config.lollipopVisible) {
                const lollipopX = lollipopStartX + lollipopPositions[featureIndex];
                const lollipopPath = [
                    `M ${lollipopX},${this.config.lollipopHeight / 8}`,
                    `L ${lollipopX},${this.config.lollipopHeight / 2}`,
                    `L ${x},${3 * this.config.lollipopHeight / 4}`,
                    `L ${x},${this.config.lollipopHeight}`,
                ];
                // Render lollipop stick
                SVG.addChild(group, "path", {
                    "d": lollipopPath.join(" "),
                    "fill": "transparent",
                    "stroke": this.config.lollipopStickColor,
                    "stroke-width": this.config.lollipopStickWidth,
                });
                // Lollipop shape
                variantElement = SVG.addChild(group, "circle", {
                    "cx": lollipopX,
                    "cy": this.config.lollipopHeight / 8,
                    "r": 5,
                    "fill": variantColor,
                });

            } else {
                variantElement = SVG.addChild(group, "rect", {
                    "x": x,
                    "y": 0,
                    "width": `${width}px`,
                    "height": `${this.config.headerHeight - this.config.dividerHeight - 1}px`,
                    "fill": variantColor,
                    "cursor": "pointer",
                });
            }

            // Display tooltip
            if (variantElement && variantTooltipText) {
                $(variantElement).qtip({
                    content: {
                        text: variantTooltipText,
                        title: variantTooltipTitle || null,
                    },
                    position: {
                        viewport: window,
                        target: "mouse",
                    },
                    style: {
                        width: true,
                        classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                    },
                });
            }

            // Render for each sample in feature.studies[0].samples
            // this.config.sampleNames.forEach((sampleName, index) => {
            (feature.studies[0]?.samples || []).forEach((sampleData, index) => {
                // Only one study is expected, and GT is always the first field in samplesData
                const genotype = sampleData.data[0];
                const sampleGenotypeColor = this.getValueFromConfig("sampleGenotypeColor", [genotype]);
                const sampleGenotypeTooltipTitle = this.getValueFromConfig("sampleGenotypeTooltipTitle", [feature, sampleData]);
                const sampleGenotypeTooltipText = this.getValueFromConfig("sampleGenotypeTooltipText", [feature, sampleData]);

                const sampleGenotypeElement = SVG.addChild(group, "rect", {
                    "x": x,
                    "y": topPosition + (index * this.config.sampleHeight) + 1,
                    "width": width,
                    "height": this.config.sampleHeight - 2,
                    // "stroke": "#000000",
                    // "stroke-width": 1,
                    // "stroke-opacity": 0.7,
                    "fill": sampleGenotypeColor,
                    "cursor": "pointer",
                    // "data-genotype": genotype,
                });

                if (sampleGenotypeTooltipText) {
                    $(sampleGenotypeElement).qtip({
                        content: {
                            text: sampleGenotypeTooltipText,
                            title: sampleGenotypeTooltipTitle || null,
                        },
                        position: {
                            viewport: window,
                            target: "mouse",
                        },
                        style: {
                            width: true,
                            classes: `${this.config.toolTipfontClass} ui-tooltip ui-tooltip-shadow`,
                        },
                    });
                }
            });
        });
    }

    getDefaultConfig() {
        return {
            infoWidgetId: "id",
            strokeColor: "#555",
            height: 10,
            histogramColor: "#58f3f0",
            // label: GenomeBrowserUtils.variantLabelFormatter,
            variantColor: GenomeBrowserUtils.variantColorFormatter,
            variantTooltipTitle: GenomeBrowserUtils.variantTooltipTitleFormatter,
            variantTooltipText: GenomeBrowserUtils.variantTooltipTextFormatter,
            sampleGenotypeColor: GenomeBrowserUtils.genotypeColorFormatter,
            sampleGenotypeTooltipTitle: GenomeBrowserUtils.sampleGenotypeTooltipTitleFormatter,
            sampleGenotypeTooltipText: GenomeBrowserUtils.sampleGenotypeTooltipTextFormatter,
            // sampleTrackHeight: 20,
            // sampleTrackY: 15,
            // sampleTrackFontSize: 12,
            sampleNames: [],
            sampleHeight: 40,
            // Header configuration
            headerHeight: 20,
            dividerHeight: 2,
            // Lollipop configuration
            lollipopVisible: true,
            lollipopHeight: 40,
            lollipopStickColor: "rgb(164,171,182)",
            lollipopStickWidth: 1,
            lollipopMaxWidth: 10,
        };
    }

}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';

export default class UploadArea extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    onClose: PropTypes.func,
  };

  handleKeyUp = (e) => {
    e.preventDefault();
    e.stopPropagation();

    const keyCode = e.keyCode;
    if (this.props.active) {
      switch(keyCode) {
      case 27:
        this.props.onClose();
        break;
      }
    }
  }

  componentDidMount () {
    window.addEventListener('keyup', this.handleKeyUp, false);
  }

  componentWillUnmount () {
    window.removeEventListener('keyup', this.handleKeyUp);
  }

  render () {
    const { active } = this.props;

    return (
      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
        {({ backgroundOpacity, backgroundScale }) =>

              <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />

<FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' />

        }
      </Motion>
    );
  }

}
<commit_msg>UploadArea should only preventDefault for Escape<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring';
import { FormattedMessage } from 'react-intl';

export default class UploadArea extends React.PureComponent {

  static propTypes = {
    active: PropTypes.bool,
    onClose: PropTypes.func,
  };

  handleKeyUp = (e) => {
    const keyCode = e.keyCode;
    if (this.props.active) {
      switch(keyCode) {
      case 27:
        e.preventDefault();
        e.stopPropagation();
        this.props.onClose();
        break;
      }
    }
  }

  componentDidMount () {
    window.addEventListener('keyup', this.handleKeyUp, false);
  }

  componentWillUnmount () {
    window.removeEventListener('keyup', this.handleKeyUp);
  }

  render () {
    const { active } = this.props;

    return (
      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
        {({ backgroundOpacity, backgroundScale }) =>

              <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />

<FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' />

        }
      </Motion>
    );
  }

}
<|endoftext|>


<commit_before>import { expect } from 'chai';
import config from '../config';
import NotifyActions from '../NotifyActions';
import Item from '../Item';

describe('Item object', () => {
  var clock;
  beforeEach(function () { clock = sinon.useFakeTimers(); });
  afterEach(function () { clock.restore(); });

  describe('#constructor', () => {
    it('should start the timer when instantiated', () => {
      sinon.spy(window, 'setTimeout');

      // It's called on the constructor so we'll just instantiate
      var item = new Item();
      expect(window.setTimeout.called).to.equal(true);

      window.setTimeout.restore();
    });
  });

  // We really just wanna test the `remove` here.
  // Compared to above,
  describe('#remove', () => {
    it('should cancel the timer for itself after the provided duration', () => {
      const duration = 1000;
      sinon.spy(window, 'clearTimeout');
      // We don't really need to test the `removeHandler`
      // as this is tested on the test below
      var item = new Item({ duration });

      clock.tick(duration);
      expect(clearTimeout.called).to.equal(true);

      window.clearTimeout.restore();
    });

    it('should call `remove` within the <config defaults> if the <duration> was not provided', () => {
      const duration = 10000;
      sinon.stub(config, 'duration').returns(duration);
      // We don't really need to test the `removeHandler`
      // as this is tested on the test below
      sinon.spy(Item.prototype, 'remove');
      var item = new Item({});

      clock.tick(duration);
      expect(item.remove.called).to.equal(true);

      Item.prototype.remove.restore();
      config.duration.restore();
    });

    it('should call the NotifyActions.remove with the given id', () => {
      var spy = sinon.spy(NotifyActions, 'remove');
      var item = new Item({});

      item.remove();
      expect(NotifyActions.remove.called).to.equal(true);
      NotifyActions.remove.restore()
    });
  });
});
<commit_msg>Refactored tests to es6 and sinon sandbox<commit_after>import { expect } from 'chai';
import config from '../config';
import NotifyActions from '../NotifyActions';
import Item from '../Item';

describe('Item object', () => {
  var clock, sandbox;
  beforeEach(() => {
    sandbox = sinon.sanbox.create();
    clock = sandbox.useFakeTimers();
  });
  afterEach(() => {
    sandbox.restore();
  });

  describe('#constructor', () => {
    it('should start the timer when instantiated', () => {
      sandbox.spy(window, 'setTimeout');

      // It's called on the constructor so we'll just instantiate
      var item = new Item();
      expect(window.setTimeout.called).to.equal(true);
    });
  });

  // We really just wanna test the `remove` here.
  // Compared to above,
  describe('#remove', () => {
    it('should cancel the timer for itself after the provided duration', () => {
      const duration = 1000;
      sandbox.spy(window, 'clearTimeout');
      // We don't really need to test the `removeHandler`
      // as this is tested on the test below
      var item = new Item({ duration });

      clock.tick(duration);
      expect(clearTimeout.called).to.equal(true);
    });

    it('should call `remove` within the <config defaults> if the <duration> was not provided', () => {
      const duration = 10000;
      sandbox.stub(config, 'duration').returns(duration);
      // We don't really need to test the `removeHandler`
      // as this is tested on the test below
      sandbox.spy(Item.prototype, 'remove');
      var item = new Item({});

      clock.tick(duration);
      expect(item.remove.called).to.equal(true);
    });

    it('should call the NotifyActions.remove with the given id', () => {
      var spy = sandbox.spy(NotifyActions, 'remove');
      var item = new Item({});

      item.remove();
      expect(NotifyActions.remove.called).to.equal(true);
    });
  });
});<|endoftext|>


<commit_before>// Copyright 2015, University of Colorado Boulder

/**
 * Tandem is used to assign unique identifiers to instances in PhET simulations and register/unregister them in a
 * registry. It is used to support PhET-iO.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var tandemNamespace = require( 'TANDEM/tandemNamespace' );
  var toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  var packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  var packageJSON = JSON.parse( packageString ); // Tandem can't depend on joist, so requiring packageJSON doesn't work
  var PHET_IO_ENABLED = !!( window.phet && window.phet.phetio );

  // used to keep track of missing tandems, see phet.phetio.queryParameters.printMissingTandems
  var missingTandems = {
    required: [],
    optional: [],
    uninstrumented: []
  };

  // Listeners that will be notified when items are registered/deregistered. See doc in addInstanceListener
  var instanceListeners = [];

  // variables
  // Before listeners are wired up, tandems are buffered.  When listeners are wired up, Tandem.launch() is called
  // and buffered tandems are flushed, then subsequent tandems are delivered to listeners directly
  var launched = false;
  var bufferedElements = [];

  /**
   * Typically, sims will create tandems using `tandem.createTandem`.  This constructor is used internally or when
   * a tandem must be created from scratch.
   *
   * @param {string} phetioID - unique identifier as a string ('' for a root id)
   * @param {Object} [options]
   * @constructor
   */
  function Tandem( phetioID, options ) {

    // options (even subtype options) must be stored on the instance so they can be passed through to children
    // Note: Make sure that added options here are also added to options for inheritance and/or
    // for composition (createTandem/parentTandem) as they make sense.
    options = _.extend( {

      // if the tandem is not supplied and required, an error will be thrown.
      supplied: true,

      // required === false means it is an optional tandem
      required: true
    }, options );

    // @public (read-only)
    this.phetioID = ( phetioID !== undefined ) ? phetioID : '';

    // @private
    this.required = options.required;

    // @private
    this.supplied = options.supplied;
  }

  tandemNamespace.register( 'Tandem', Tandem );

  inherit( Object, Tandem, {

    /**
     * Adds an instance of any type.  For example, it could be an axon Property, scenery Node or Sun button.  Each
     * item should only be added to the registry once, but that is not enforced here in Tandem.  For PhET-iO, phetioEngine.js
     * enforces one entry per ID in phetio.addInstance
     *
     * This is used to register instances with PhET-iO.
     * @param {PhetioObject} instance - the instance to add
     * @public
     */
    addInstance: function( instance ) {

      assert && assert( arguments.length === 1, 'Tandem.addInstance takes one argument' );

      // Check that the instrumented instance is an instance of PhetioObject, cannot use typical require statement for
      // PhetioObject because it creates a module loading loop
      assert && assert( instance instanceof tandemNamespace.PhetioObject, 'Instance should be of type PhetioObject' );

      if ( PHET_IO_ENABLED ) {

        // Throw an error if the tandem is required but not supplied
        if ( Tandem.validationEnabled() ) {
          assert && assert( !( this.required && !this.supplied ), 'Tandem was required but not supplied' );
        }

        // printMissingTandems flag is present for a tandem that is required but not supplied.
        if ( phet.phetio.queryParameters.printMissingTandems && ( this.required && !this.supplied ) ) {
          missingTandems.required.push( { phetioID: this.phetioID, stack: new Error().stack } );
        }

        // If tandem is optional, then don't add the instance
        if ( !this.required && !this.supplied ) {
          if ( phet.phetio.queryParameters.printMissingTandems ) {
            var stackTrace = new Error().stack;

            // Generally Font is not desired because there are so many untandemized instances.
            if ( stackTrace.indexOf( 'PhetFont' ) === -1 ) {
              missingTandems.optional.push( { phetioID: this.phetioID, stack: stackTrace } );
            }
          }

          // For optionally instrumented types that are not provided tandems, the instance isn't really "added"
          // but likewise, it in not an error
          return;
        }

        if ( !launched ) {
          bufferedElements.push( instance );
        }
        else {
          for ( var i = 0; i < instanceListeners.length; i++ ) {
            instanceListeners[ i ].addInstance( instance );
          }
        }
      }
    },

    /**
     * Removes an instance from the registry
     * @param {PhetioObject} instance - the instance to remove
     * @public
     */
    removeInstance: function( instance ) {
      if ( !this.required && !this.supplied ) {
        return;
      }

      // Only active when running as phet-io
      if ( PHET_IO_ENABLED ) {
        for ( var i = 0; i < instanceListeners.length; i++ ) {
          instanceListeners[ i ].removeInstance( instance );
        }
      }
    },

    /**
     * Create a new Tandem by appending the given id
     * @param {string} id
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem: function( id, options ) {

      // Make sure the id was provided
      assert && assert( typeof id === 'string' && id.length > 0, 'id must be defined' );

      var string = ( this.phetioID.length > 0 ) ? phetio.PhetioIDUtils.append( this.phetioID, id ) : id;

      // Any child of something should be passed all inherited options. Make sure that this extend call includes all
      // that make sense from the constructor's extend call.
      options = _.extend( {
        supplied: this.supplied,
        required: this.required
      }, options );

      return new Tandem( string, options );
    },

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electron_0
     * sim.screen.model.electron_1
     *
     * In this case, 'sim.screen.model.electron' is the string passed to createGroupTandem.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param id
     * @returns {GroupTandem}
     * @public
     */
    createGroupTandem: function( id ) {

      // Unfortunately we must resort to globals here since loading through the namespace would create a cycle
      return new GroupTandem( phetio.PhetioIDUtils.append( this.phetioID, id ) );
    },

    /**
     * Get the last part of the tandem (after the last .), used in Joist for creating button names dynamically based
     * on screen names
     * @returns {string} the tail of the tandem
     * @public
     */
    get tail() { // TODO: rename to getComponentName()
      return phetio.PhetioIDUtils.getComponentName( this.phetioID );
    },

    /**
     * Returns a Tandem for everything except the tail.
     * @returns {Tandem}
     * @public
     */
    get parentTandem() {
      var headID = phetio.PhetioIDUtils.getParentID( this.phetioID );

      return new Tandem( headID, {
        required: this.required,
        supplied: this.supplied
      } );
    }
  }, {

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addInstance(id,instance),
     *   removeInstance(id,instance)
     * }
     * where id is of type {string} and instance is of type {Object}
     *
     * @param {Object} instanceListener - described above
     * @public
     * @static
     */
    addInstanceListener: function( instanceListener ) {
      instanceListeners.push( instanceListener );
    },

    /**
     * When all listeners are listening, all buffered instances are registered.
     * @public
     * @static
     */
    launch: function() {
      assert && assert( !launched, 'Tandem was launched twice' );
      launched = true;
      while ( bufferedElements.length > 0 ) {
        var element = bufferedElements.shift();
        element.tandem.addInstance( element );
      }
    },

    /**
     * Catch cases where tandem is being supplied to a class that doesn't support tandem.
     * @param {Object} [options]
     * @public
     * @static
     */
    disallowTandem: function( options ) {

      if ( Tandem.validationEnabled() ) {
        assert && assert( !options.tandem, 'tandem is not allowed' );
      }
    },

    /**
     * When running in PhET-iO brand, some code (such as user interface components) must be instrumented for PhET-iO.
     * Uninstrumented files should call this function to indicate they still need to be instrumented, so they aren't
     * missed.  See https://github.com/phetsims/phet-io/issues/668
     * @public
     * @static
     */
    indicateUninstrumentedCode: function() {

      // Guard against undefined errors
      if ( PHET_IO_ENABLED ) {

        // Assert if validating tandems
        if ( this.validationEnabled() ) {
          assert && assert( false, 'Uninstrumented code detected' );
        }

        // Print stack trace if query parameter supplied
        if ( phet.phetio.queryParameters.printMissingTandems ) {
          missingTandems.uninstrumented.push( { stack: new Error().stack } );
        }
      }
    },

    /**
     * Determine whether or not tandem validation is turned on for the sim.
     * @returns {Boolean} If tandems are being validated or not.
     * @public
     * @static
     */
    validationEnabled: function() {
      return PHET_IO_ENABLED && phet.phetio.queryParameters.phetioValidateTandems && !phet.phetio.queryParameters.printMissingTandems;
    },

    /**
     * Returns true if the bufferedElements contain the given phetioID.
     * @param {string} phetioID
     * @returns {boolean}
     * @public
     */
    containsBufferedID: function( phetioID ) {
      return bufferedElements.map( function( instance ) {return instance.tandem.phetioID;} ).indexOf( phetioID ) >= 0;
    }
  } );

  // The next few statics are created outside the static block because they instantiate Tandem instances.

  /**
   * The root tandem for a simulation
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.rootTandem = new Tandem( toCamelCase( packageJSON.name ) );

  /**
   * Used to indicate a common code component that supports tandem, but doesn't not require it.
   * If a tandem is not passed through to this instance, then it will not be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optional = Tandem.rootTandem.createTandem( 'optionalTandem', {
    required: false,
    supplied: false
  } );

  /**
   * Some common code (such as Checkbox or RadioButton) must always be instrumented and hence requires a tandem to be
   * passed in.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.required = Tandem.rootTandem.createTandem( 'requiredTandem', {

    // let printMissingTandems bypass this
    required: PHET_IO_ENABLED && ( phet.phetio.queryParameters.phetioValidateTandems || phet.phetio.queryParameters.printMissingTandems ),
    supplied: false
  } );

  /**
   * Expose collected missing tandems only populated from specific query parameter, see printMissingTandems for more
   * @public (phet-io internal)
   * @type {Object}
   */
  Tandem.missingTandems = missingTandems;

  /**
   * Group Tandem -- Declared in the same file to avoid circular reference errors in module loading.
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createGroupTandem
   */
  function GroupTandem( id ) {

    Tandem.call( this, id );

    // @private for generating indices from a pool
    this.groupElementIndex = 0;
  }

  tandemNamespace.register( 'Tandem.GroupTandem', GroupTandem );

  inherit( Tandem, GroupTandem, {

    /**
     * Creates the next tandem in the group.
     * @returns {Tandem}
     * @public
     */
    createNextTandem: function() {
      return new Tandem( this.phetioID + '_' + ( this.groupElementIndex++ ) );
    }
  } );

  return Tandem;
} );<commit_msg>Added docs to validationEnabled, see https://github.com/phetsims/phet-io/issues/1369<commit_after>// Copyright 2015, University of Colorado Boulder

/**
 * Tandem is used to assign unique identifiers to instances in PhET simulations and register/unregister them in a
 * registry. It is used to support PhET-iO.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var inherit = require( 'PHET_CORE/inherit' );
  var tandemNamespace = require( 'TANDEM/tandemNamespace' );
  var toCamelCase = require( 'PHET_CORE/toCamelCase' );

  // text
  var packageString = require( 'text!REPOSITORY/package.json' );

  // constants
  var packageJSON = JSON.parse( packageString ); // Tandem can't depend on joist, so requiring packageJSON doesn't work
  var PHET_IO_ENABLED = !!( window.phet && window.phet.phetio );

  // used to keep track of missing tandems, see phet.phetio.queryParameters.printMissingTandems
  var missingTandems = {
    required: [],
    optional: [],
    uninstrumented: []
  };

  // Listeners that will be notified when items are registered/deregistered. See doc in addInstanceListener
  var instanceListeners = [];

  // variables
  // Before listeners are wired up, tandems are buffered.  When listeners are wired up, Tandem.launch() is called
  // and buffered tandems are flushed, then subsequent tandems are delivered to listeners directly
  var launched = false;
  var bufferedElements = [];

  /**
   * Typically, sims will create tandems using `tandem.createTandem`.  This constructor is used internally or when
   * a tandem must be created from scratch.
   *
   * @param {string} phetioID - unique identifier as a string ('' for a root id)
   * @param {Object} [options]
   * @constructor
   */
  function Tandem( phetioID, options ) {

    // options (even subtype options) must be stored on the instance so they can be passed through to children
    // Note: Make sure that added options here are also added to options for inheritance and/or
    // for composition (createTandem/parentTandem) as they make sense.
    options = _.extend( {

      // if the tandem is not supplied and required, an error will be thrown.
      supplied: true,

      // required === false means it is an optional tandem
      required: true
    }, options );

    // @public (read-only)
    this.phetioID = ( phetioID !== undefined ) ? phetioID : '';

    // @private
    this.required = options.required;

    // @private
    this.supplied = options.supplied;
  }

  tandemNamespace.register( 'Tandem', Tandem );

  inherit( Object, Tandem, {

    /**
     * Adds an instance of any type.  For example, it could be an axon Property, scenery Node or Sun button.  Each
     * item should only be added to the registry once, but that is not enforced here in Tandem.  For PhET-iO, phetioEngine.js
     * enforces one entry per ID in phetio.addInstance
     *
     * This is used to register instances with PhET-iO.
     * @param {PhetioObject} instance - the instance to add
     * @public
     */
    addInstance: function( instance ) {

      assert && assert( arguments.length === 1, 'Tandem.addInstance takes one argument' );

      // Check that the instrumented instance is an instance of PhetioObject, cannot use typical require statement for
      // PhetioObject because it creates a module loading loop
      assert && assert( instance instanceof tandemNamespace.PhetioObject, 'Instance should be of type PhetioObject' );

      if ( PHET_IO_ENABLED ) {

        // Throw an error if the tandem is required but not supplied
        if ( Tandem.validationEnabled() ) {
          assert && assert( !( this.required && !this.supplied ), 'Tandem was required but not supplied' );
        }

        // printMissingTandems flag is present for a tandem that is required but not supplied.
        if ( phet.phetio.queryParameters.printMissingTandems && ( this.required && !this.supplied ) ) {
          missingTandems.required.push( { phetioID: this.phetioID, stack: new Error().stack } );
        }

        // If tandem is optional, then don't add the instance
        if ( !this.required && !this.supplied ) {
          if ( phet.phetio.queryParameters.printMissingTandems ) {
            var stackTrace = new Error().stack;

            // Generally Font is not desired because there are so many untandemized instances.
            if ( stackTrace.indexOf( 'PhetFont' ) === -1 ) {
              missingTandems.optional.push( { phetioID: this.phetioID, stack: stackTrace } );
            }
          }

          // For optionally instrumented types that are not provided tandems, the instance isn't really "added"
          // but likewise, it in not an error
          return;
        }

        if ( !launched ) {
          bufferedElements.push( instance );
        }
        else {
          for ( var i = 0; i < instanceListeners.length; i++ ) {
            instanceListeners[ i ].addInstance( instance );
          }
        }
      }
    },

    /**
     * Removes an instance from the registry
     * @param {PhetioObject} instance - the instance to remove
     * @public
     */
    removeInstance: function( instance ) {
      if ( !this.required && !this.supplied ) {
        return;
      }

      // Only active when running as phet-io
      if ( PHET_IO_ENABLED ) {
        for ( var i = 0; i < instanceListeners.length; i++ ) {
          instanceListeners[ i ].removeInstance( instance );
        }
      }
    },

    /**
     * Create a new Tandem by appending the given id
     * @param {string} id
     * @param {Object} [options]
     * @returns {Tandem}
     * @public
     */
    createTandem: function( id, options ) {

      // Make sure the id was provided
      assert && assert( typeof id === 'string' && id.length > 0, 'id must be defined' );

      var string = ( this.phetioID.length > 0 ) ? phetio.PhetioIDUtils.append( this.phetioID, id ) : id;

      // Any child of something should be passed all inherited options. Make sure that this extend call includes all
      // that make sense from the constructor's extend call.
      options = _.extend( {
        supplied: this.supplied,
        required: this.required
      }, options );

      return new Tandem( string, options );
    },

    /**
     * Creates a group tandem for creating multiple indexed child tandems, such as:
     * sim.screen.model.electron_0
     * sim.screen.model.electron_1
     *
     * In this case, 'sim.screen.model.electron' is the string passed to createGroupTandem.
     *
     * Used for arrays, observable arrays, or when many elements of the same type are created and they do not otherwise
     * have unique identifiers.
     * @param id
     * @returns {GroupTandem}
     * @public
     */
    createGroupTandem: function( id ) {

      // Unfortunately we must resort to globals here since loading through the namespace would create a cycle
      return new GroupTandem( phetio.PhetioIDUtils.append( this.phetioID, id ) );
    },

    /**
     * Get the last part of the tandem (after the last .), used in Joist for creating button names dynamically based
     * on screen names
     * @returns {string} the tail of the tandem
     * @public
     */
    get tail() { // TODO: rename to getComponentName()
      return phetio.PhetioIDUtils.getComponentName( this.phetioID );
    },

    /**
     * Returns a Tandem for everything except the tail.
     * @returns {Tandem}
     * @public
     */
    get parentTandem() {
      var headID = phetio.PhetioIDUtils.getParentID( this.phetioID );

      return new Tandem( headID, {
        required: this.required,
        supplied: this.supplied
      } );
    }
  }, {

    /**
     * Adds a listener that will be notified when items are registered/deregistered
     * Listeners have the form
     * {
     *   addInstance(id,instance),
     *   removeInstance(id,instance)
     * }
     * where id is of type {string} and instance is of type {Object}
     *
     * @param {Object} instanceListener - described above
     * @public
     * @static
     */
    addInstanceListener: function( instanceListener ) {
      instanceListeners.push( instanceListener );
    },

    /**
     * When all listeners are listening, all buffered instances are registered.
     * @public
     * @static
     */
    launch: function() {
      assert && assert( !launched, 'Tandem was launched twice' );
      launched = true;
      while ( bufferedElements.length > 0 ) {
        var element = bufferedElements.shift();
        element.tandem.addInstance( element );
      }
    },

    /**
     * Catch cases where tandem is being supplied to a class that doesn't support tandem.
     * @param {Object} [options]
     * @public
     * @static
     */
    disallowTandem: function( options ) {

      if ( Tandem.validationEnabled() ) {
        assert && assert( !options.tandem, 'tandem is not allowed' );
      }
    },

    /**
     * When running in PhET-iO brand, some code (such as user interface components) must be instrumented for PhET-iO.
     * Uninstrumented files should call this function to indicate they still need to be instrumented, so they aren't
     * missed.  See https://github.com/phetsims/phet-io/issues/668
     * @public
     * @static
     */
    indicateUninstrumentedCode: function() {

      // Guard against undefined errors
      if ( PHET_IO_ENABLED ) {

        // Assert if validating tandems
        if ( this.validationEnabled() ) {
          assert && assert( false, 'Uninstrumented code detected' );
        }

        // Print stack trace if query parameter supplied
        if ( phet.phetio.queryParameters.printMissingTandems ) {
          missingTandems.uninstrumented.push( { stack: new Error().stack } );
        }
      }
    },

    /**
     * Determine whether or not tandem validation is turned on for the sim.
     * @returns {Boolean} If tandems are being validated or not.
     * @public
     * @static
     */
    validationEnabled: function() {
      return PHET_IO_ENABLED &&
             phet.phetio.queryParameters.phetioValidateTandems &&

             // If we are printing the missing tandems, then validation must be disabled because the intention is to
             // run with partial tandem coverage and see which are missing.
             !phet.phetio.queryParameters.printMissingTandems;
    },

    /**
     * Returns true if the bufferedElements contain the given phetioID.
     * @param {string} phetioID
     * @returns {boolean}
     * @public
     */
    containsBufferedID: function( phetioID ) {
      return bufferedElements.map( function( instance ) {return instance.tandem.phetioID;} ).indexOf( phetioID ) >= 0;
    }
  } );

  // The next few statics are created outside the static block because they instantiate Tandem instances.

  /**
   * The root tandem for a simulation
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.rootTandem = new Tandem( toCamelCase( packageJSON.name ) );

  /**
   * Used to indicate a common code component that supports tandem, but doesn't not require it.
   * If a tandem is not passed through to this instance, then it will not be instrumented.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.optional = Tandem.rootTandem.createTandem( 'optionalTandem', {
    required: false,
    supplied: false
  } );

  /**
   * Some common code (such as Checkbox or RadioButton) must always be instrumented and hence requires a tandem to be
   * passed in.
   * @public
   * @static
   * @type {Tandem}
   */
  Tandem.required = Tandem.rootTandem.createTandem( 'requiredTandem', {

    // let printMissingTandems bypass this
    required: PHET_IO_ENABLED && ( phet.phetio.queryParameters.phetioValidateTandems || phet.phetio.queryParameters.printMissingTandems ),
    supplied: false
  } );

  /**
   * Expose collected missing tandems only populated from specific query parameter, see printMissingTandems for more
   * @public (phet-io internal)
   * @type {Object}
   */
  Tandem.missingTandems = missingTandems;

  /**
   * Group Tandem -- Declared in the same file to avoid circular reference errors in module loading.
   * @param {string} id - id as a string (or '' for a root id)
   * @constructor
   * @private create with Tandem.createGroupTandem
   */
  function GroupTandem( id ) {

    Tandem.call( this, id );

    // @private for generating indices from a pool
    this.groupElementIndex = 0;
  }

  tandemNamespace.register( 'Tandem.GroupTandem', GroupTandem );

  inherit( Tandem, GroupTandem, {

    /**
     * Creates the next tandem in the group.
     * @returns {Tandem}
     * @public
     */
    createNextTandem: function() {
      return new Tandem( this.phetioID + '_' + ( this.groupElementIndex++ ) );
    }
  } );

  return Tandem;
} );<|endoftext|>